diff --git a/.github/workflows/test-coverage.yaml b/.github/workflows/test-coverage.yaml new file mode 100644 index 0000000..e050312 --- /dev/null +++ b/.github/workflows/test-coverage.yaml @@ -0,0 +1,61 @@ +# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples +# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help +on: + push: + branches: [main, master] + pull_request: + +name: test-coverage.yaml + +permissions: read-all + +jobs: + test-coverage: + runs-on: ubuntu-latest + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + + steps: + - uses: actions/checkout@v4 + + - uses: r-lib/actions/setup-r@v2 + with: + use-public-rspm: true + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: any::covr, any::xml2 + needs: coverage + + - name: Test coverage + run: | + cov <- covr::package_coverage( + quiet = FALSE, + clean = FALSE, + install_path = file.path(normalizePath(Sys.getenv("RUNNER_TEMP"), winslash = "/"), "package") + ) + covr::to_cobertura(cov) + shell: Rscript {0} + + - uses: codecov/codecov-action@v4 + with: + # Fail if error if not on PR, or if on PR and token is given + fail_ci_if_error: ${{ github.event_name != 'pull_request' || secrets.CODECOV_TOKEN }} + file: ./cobertura.xml + plugin: noop + disable_search: true + token: ${{ secrets.CODECOV_TOKEN }} + + - name: Show testthat output + if: always() + run: | + ## -------------------------------------------------------------------- + find '${{ runner.temp }}/package' -name 'testthat.Rout*' -exec cat '{}' \; || true + shell: bash + + - name: Upload test results + if: failure() + uses: actions/upload-artifact@v4 + with: + name: coverage-test-failures + path: ${{ runner.temp }}/package diff --git a/DESCRIPTION b/DESCRIPTION index 79caaf6..6190c30 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -35,7 +35,7 @@ Suggests: languageserver (>= 0.3.16) Config/testthat/edition: 3 URL: https://teal-insights.github.io/r-wbids, https://github.com/teal-insights/r-wbids -BugReports: https://github.com/Teal-Insights/r-wbids/issues +BugReports: https://github.com/teal-insights/r-wbids/issues VignetteBuilder: quarto Remotes: r-lib/devtools, diff --git a/NAMESPACE b/NAMESPACE index 2a56817..989bc74 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -12,6 +12,7 @@ import(cli) import(dplyr) import(httr2) importFrom(purrr,map) +importFrom(purrr,map_df) importFrom(rlang,eval_tidy) importFrom(rlang,parse_expr) importFrom(tidyr,crossing) diff --git a/R/ids_bulk.R b/R/ids_bulk.R index 5ca2dd5..86a788e 100644 --- a/R/ids_bulk.R +++ b/R/ids_bulk.R @@ -43,23 +43,20 @@ ids_bulk <- function( timeout = getOption("timeout", 60), warn_size = TRUE ) { + rlang::check_installed("readxl", reason = "to download bulk files.") - # Register cleanup immediately after creating temporary file on.exit(unlink(file_path)) - # Download file with size checks and validation download_bulk_file(file_url, file_path, timeout, warn_size, quiet) - # Read and process the data - if (!quiet) message("Reading in file.") + if (!quiet) cli::cli_progress_message("Reading in file.") bulk_data <- read_bulk_file(file_path) - if (!quiet) message("Processing file.") + if (!quiet) cli::cli_progress_message("Processing file.") bulk_data <- process_bulk_data(bulk_data) - # Return the processed data - return(bulk_data) + bulk_data } #' Get response headers from a URL @@ -81,39 +78,33 @@ get_response_headers <- function(file_url) { #' @param warn_size Whether to warn about large files #' @param quiet Whether to suppress messages download_bulk_file <- function(file_url, file_path, timeout, warn_size, quiet) { - # Get file size before downloading + response_headers <- get_response_headers(file_url) size_mb <- as.numeric(response_headers$`content-length`) / 1024^2 - formatted_size <- format(round(size_mb, 1), nsmall = 1) + formatted_size <- format(round(size_mb, 1), nsmall = 1) # nolint if (warn_size && size_mb > 100) { - warning( - sprintf( - paste( - "This file is %s MB and may take several minutes to download.", - "Current timeout setting: %s seconds.", - "Use warn_size=FALSE to disable this warning.", - sep = "\n" - ), - formatted_size, - timeout - ), - call. = FALSE - ) - - # Interactive confirmation + cli::cli_warn(paste0( + "This file is {formatted_size} MB and may take several minutes to ", + "download. Current timeout setting: {timeout} seconds. Use ", + "{.code warn_size = FALSE} to disable this warning." + )) + if (warn_size && check_interactive()) { - response <- readline("Do you want to continue with the download? (y/N): ") + response <- prompt_user( + "Do you want to continue with the download? (y/N): " + ) if (!tolower(response) %in% c("y", "yes")) { - stop("Download cancelled by user", call. = FALSE) + cli::cli_abort("Download cancelled by user") } } } - # Print message about file download - if (!quiet) message("Downloading file to: {file_path}") + if (!quiet) { + cli::cli_progress_message("Downloading file to: {file_path}") + } - # Download with timeout handling + # nocov start withr::with_options( list(timeout = timeout), tryCatch({ @@ -121,20 +112,18 @@ download_bulk_file <- function(file_url, file_path, timeout, warn_size, quiet) { }, error = function(e) { if (grepl("timeout|cannot open URL", e$message, ignore.case = TRUE)) { - stop( + cli::cli_abort( paste0( "Download timed out after ", timeout, " seconds.\n", "Try increasing the timeout parameter", " (e.g., timeout=600 for 10 minutes)" - ), - call. = FALSE + ) ) } - stop(e$message, call. = FALSE) + cli::cli_abort(e$message) }) ) - - # Validate downloaded file + # nocov end validate_file(file_path) } @@ -143,11 +132,11 @@ download_bulk_file <- function(file_url, file_path, timeout, warn_size, quiet) { #' @param file_path Path to file to validate validate_file <- function(file_path) { if (!file.exists(file_path)) { - stop("Download failed: File not created") + cli::cli_abort("Download failed: File not created") } if (file.size(file_path) == 0) { unlink(file_path) - stop("Download failed: Empty file") + cli::cli_abort("Download failed: Empty file") } } @@ -215,3 +204,13 @@ check_interactive <- function() { download_file <- function(url, destfile, quiet) { utils::download.file(url, destfile = destfile, quiet = quiet, mode = "wb") } + +#' Prompt a user with a question +#' +#' Wrapper around base::readline to facilitate testing. Cannot be tested +#' because of the base binding. +#' +#' @keywords internal +prompt_user <- function(prompt) { + readline(prompt) # nocov +} diff --git a/R/ids_bulk_files.R b/R/ids_bulk_files.R index 9600dd0..ec3eae9 100644 --- a/R/ids_bulk_files.R +++ b/R/ids_bulk_files.R @@ -19,16 +19,7 @@ #' ids_bulk_files <- function() { - rlang::check_installed( - "jsonlite", reason = "to download bulk files." - ) - - ids_meta <- jsonlite::fromJSON( - txt = paste0( - "https://datacatalogapi.worldbank.org/ddhxext/DatasetDownload", - "?dataset_unique_id=0038015&version_id=" - ) - ) + ids_meta <- read_bulk_info() bulk_files <- ids_meta$resources |> as_tibble() |> diff --git a/R/ids_bulk_series.R b/R/ids_bulk_series.R index d8cf37f..abfded7 100644 --- a/R/ids_bulk_series.R +++ b/R/ids_bulk_series.R @@ -24,16 +24,7 @@ #' ids_bulk_series <- function() { - rlang::check_installed( - "jsonlite", reason = "to retrieve available series via bulk download." - ) - - ids_meta <- jsonlite::fromJSON( - txt = paste0( - "https://datacatalogapi.worldbank.org/ddhxext/DatasetDownload", - "?dataset_unique_id=0038015&version_id=" - ) - ) + ids_meta <- read_bulk_info() bulk_series <- ids_meta$indicators |> as_tibble() |> diff --git a/R/ids_get.R b/R/ids_get.R index a971735..1ee4ed4 100644 --- a/R/ids_get.R +++ b/R/ids_get.R @@ -122,62 +122,54 @@ get_debt_statistics <- function( series_raw <- perform_request(resource, progress = progress_message) - series_raw_rbind <- series_raw$data |> - bind_rows() - - # Since the order of list items changes across series, we cannot use - # hard-coded list paths - series_wide <- series_raw_rbind |> - select("variable") |> - tidyr::unnest_wider("variable") - - geography_ids <- series_wide |> - filter(.data$concept == "Country") |> - select(geography_id = "id") - - series_ids <- series_wide |> - filter(.data$concept == "Series") |> - select(series_id = "id") - - counterpart_ids <- series_wide |> - filter(.data$concept == "Counterpart-Area") |> - select(counterpart_id = "id") - - years <- series_wide |> - filter(.data$concept == "Time") |> - select(year = "value") |> - mutate(year = as.integer(.data$year)) - - values <- extract_values(series_raw$data, "value", "numeric") - - bind_cols( - geography_ids, - series_ids, - counterpart_ids, - years, - value = values - ) -} - -extract_values <- function(data, path, type = "character") { - path_expr <- rlang::parse_expr(path) - - fun_value <- switch( - type, - "character" = NA_character_, - "integer" = NA_integer_, - "numeric" = NA_real_, - stop("Invalid type. Must be one of 'character', 'integer', or 'numeric'.") - ) + if (length(series_raw[[1]]$variable[[1]]$concept) == 0) { + tibble( + "geography_id" = character(), + "series_id" = character(), + "counterpart_id" = character(), + "year" = integer(), + "value" = numeric() + ) + } else { + series_raw_rbind <- series_raw |> + bind_rows() + + # Since the order of list items changes across series, we cannot use + # hard-coded list paths + series_wide <- series_raw_rbind |> + select("variable") |> + tidyr::unnest_wider("variable") + + geography_ids <- series_wide |> + filter(.data$concept == "Country") |> + select(geography_id = "id") + + series_ids <- series_wide |> + filter(.data$concept == "Series") |> + select(series_id = "id") + + counterpart_ids <- series_wide |> + filter(.data$concept == "Counterpart-Area") |> + select(counterpart_id = "id") + + years <- series_wide |> + filter(.data$concept == "Time") |> + select(year = "value") |> + mutate(year = as.integer(.data$year)) + + values <- series_raw |> + purrr::map_df( + \(x) tibble(value = if (is.null(x$value)) NA_real_ else x$value) + ) - vapply(data, function(x) { - result <- rlang::eval_tidy(path_expr, x) - if (is.null(result) || length(result) == 0) { - fun_value - } else { - result - } - }, FUN.VALUE = fun_value, USE.NAMES = FALSE) + bind_cols( + geography_ids, + series_ids, + counterpart_ids, + years, + values + ) + } } validate_character_vector <- function(arg, arg_name) { diff --git a/R/ids_list_geographies.R b/R/ids_list_geographies.R index 9b5931b..50954e0 100644 --- a/R/ids_list_geographies.R +++ b/R/ids_list_geographies.R @@ -7,7 +7,8 @@ #' #' @return A tibble containing the available geographies and their attributes: #' \describe{ -#' \item{geography_id}{The unique identifier for the geography (e.g., "ZMB").} +#' \item{geography_id}{ISO 3166-1 alpha-3 code of the geography (e.g., +#' "ZMB").} #' \item{geography_name}{The standardized name of the geography (e.g., #' "Zambia").} #' \item{geography_iso2code}{ISO 3166-1 alpha-2 code of the geography (e.g., diff --git a/R/perform_request.R b/R/perform_request.R index 764bfa3..085b9c3 100644 --- a/R/perform_request.R +++ b/R/perform_request.R @@ -2,7 +2,7 @@ #' perform_request <- function( resource, - per_page = 1000, + per_page = 15000, progress = FALSE, base_url = "https://api.worldbank.org/v2/sources/6/" ) { @@ -16,12 +16,20 @@ perform_request <- function( } body <- httr2::resp_body_json(resp) - pages <- extract_pages(body) + pages <- body$pages if (pages == 1L) { - out <- extract_single_page_data(body) + out <- body$source$data } else { - out <- fetch_multiple_pages(req, pages, progress) + resps <- req |> + httr2::req_perform_iterative( + next_req = httr2::iterate_with_offset("page"), + max_reqs = pages, + progress = progress + ) + out <- resps |> + purrr::map(\(x) httr2::resp_body_json(x)$source$data) |> + unlist(recursive = FALSE, use.names = FALSE) } out } @@ -46,62 +54,24 @@ create_request <- function(base_url, resource, per_page) { is_request_error <- function(resp) { status <- httr2::resp_status(resp) - if (status >= 400L) { - return(TRUE) - } - body <- httr2::resp_body_json(resp) - if (length(body) == 1L && length(body[[1L]]$message) == 1L) { - return(TRUE) - } - FALSE -} - -handle_request_error <- function(resp) { - error_body <- check_for_body_error(resp) - cli::cli_alert_danger(paste(error_body, collapse = "\n")) -} - -check_for_body_error <- function(resp) { - content_type <- httr2::resp_content_type(resp) - if (identical(content_type, "application/json")) { - body <- httr2::resp_body_json(resp) - message_id <- body[[1]]$message[[1]]$id - message_value <- body[[1]]$message[[1]]$value - error_code <- paste("Error code:", message_id) - docs <- paste0( - "Read more at " - ) - c(error_code, message_value, docs) - } -} - -extract_pages <- function(body) { - if (length(body) == 2) { - body[[1L]]$pages + content_type <- resp_content_type(resp) + if (status >= 400L || content_type == "text/xml") { + TRUE } else { - body$pages + FALSE } } -extract_single_page_data <- function(body) { - if (length(body) == 2) { - body[[2L]] - } else { - body$source - } -} - -fetch_multiple_pages <- function(req, pages, progress) { - resps <- req |> - httr2::req_perform_iterative( - next_req = httr2::iterate_with_offset("page"), - max_reqs = pages, - progress = progress - ) - out <- resps |> - purrr::map(function(x) { - httr2::resp_body_json(x)$source - }) - unlist(out, recursive = FALSE) +handle_request_error <- function(resp) { + error_string <- as.character(httr2::resp_body_xml(resp)) + error_code <- sub('.*(.*?).*", "\\1", + error_string) + cli::cli_abort( + paste("API Error Code", error_code, ":", error_message, error_description, + collapse = "\n") + ) } diff --git a/R/read_bulk_info.R b/R/read_bulk_info.R new file mode 100644 index 0000000..6644331 --- /dev/null +++ b/R/read_bulk_info.R @@ -0,0 +1,15 @@ +#' @keywords internal +#' +read_bulk_info <- function() { + + rlang::check_installed( + "jsonlite", reason = "to retrieve available series via bulk download." + ) + + jsonlite::fromJSON( + txt = paste0( + "https://datacatalogapi.worldbank.org/ddhxext/DatasetDownload", + "?dataset_unique_id=0038015&version_id=" + ) + ) +} diff --git a/R/sysdata.rda b/R/sysdata.rda index 15ef51b..baabba7 100644 Binary files a/R/sysdata.rda and b/R/sysdata.rda differ diff --git a/R/wbids-package.R b/R/wbids-package.R index acc4bcd..bd2c300 100644 --- a/R/wbids-package.R +++ b/R/wbids-package.R @@ -5,7 +5,7 @@ "_PACKAGE" ## usethis namespace: start -#' @importFrom purrr map +#' @importFrom purrr map map_df #' @importFrom tidyr unnest unnest_wider crossing pivot_longer #' @importFrom rlang parse_expr eval_tidy ## usethis namespace: end diff --git a/README.Rmd b/README.Rmd index 46dcb09..296350a 100644 --- a/README.Rmd +++ b/README.Rmd @@ -1,63 +1,64 @@ ---- -output: github_document ---- - - - -```{r, include = FALSE} -knitr::opts_chunk$set( - collapse = TRUE, - comment = "#>", - fig.path = "man/figures/README-", - out.width = "100%" -) -``` - -# wbids - - -[![R-CMD-check](https://github.com/Teal-Insights/r-wbids/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/Teal-Insights/r-wbids/actions/workflows/R-CMD-check.yaml) - - -`wbids` is an R package that provides a modern, flexible interface for accessing the World Bank’s [International Debt Statistics (IDS)](https://datacatalog.worldbank.org/search/dataset/0038015). `wbids` allows users to download, process, and analyze IDS series for multiple geographies, counterparts and specific time periods. The package is designed to work seamlessly with World Development Indicators (WDI) provided through the `wbwdi` package. - -The `wbids` package relies on a redefinition of the original World Bank data: 'geographies' contain both countries and regions, while 'counterparts' include both counterpart areas and institutions. `wbids` provides a consistent mapping of identifiers and names across these different types. See the package vignette for more details on the data model: TODO: INSERT LINK TO VIGNETTE LATER - -This package is a product of Teal Insights and not sponsored by or affiliated with the World Bank in any way, except for the use of the World Bank IDS API. - -## Installation - -You can install the development version of `wbids` like this: - -```r -pak::pak("teal-insights/r-wbids") -``` - -## Usage - -The main function `ids_get()` provides an interface to download multiple IDS series for multiple geographies and counterparts and specific date ranges. - -```r -library(wbids) - -ids_get( - geographies = c("ZMB", "ZAF"), - series = c("DT.DOD.DPPG.CD", "BM.GSR.TOTL.CD"), - counterparts = c("216", "231"), - start_date = 2015, - end_date = 2020 -) -``` - -The package comes with prepared metadata about available series, geographies, counterparts, and topics: - -```r -ids_list_series() -ids_list_georaphies() -ids_list_counterparts() -ids_list_series_topics() -``` - -This data can be used to enrich the IDS data or facilitate data discovery. For further applications, please consult [Teal Insights’ Guide to Working with the World Bank International Debt Statistics](https://teal-insights.github.io/teal_insights_guide_to_wbids/). - -The interface and namings are fully consistent with World Development Indicators (WDI) data provided through the 'wbwdi' package. You can find details on [github.com/tidy-intelligence/r-wbwdi](https://github.com/tidy-intelligence/r-wbwdi). +--- +output: github_document +--- + + + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>", + fig.path = "man/figures/README-", + out.width = "100%" +) +``` + +# wbids + + +[![R-CMD-check](https://github.com/Teal-Insights/r-wbids/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/Teal-Insights/r-wbids/actions/workflows/R-CMD-check.yaml) +[![Codecov test coverage](https://codecov.io/gh/Teal-Insights/r-wbids/graph/badge.svg)](https://app.codecov.io/gh/Teal-Insights/r-wbids) + + +`wbids` is an R package that provides a modern, flexible interface for accessing the World Bank’s [International Debt Statistics (IDS)](https://datacatalog.worldbank.org/search/dataset/0038015). `wbids` allows users to download, process, and analyze IDS series for multiple geographies, counterparts and specific time periods. The package is designed to work seamlessly with World Development Indicators (WDI) provided through the `wbwdi` package. + +The `wbids` package relies on a redefinition of the original World Bank data: 'geographies' contain both countries and regions, while 'counterparts' include both counterpart areas and institutions. `wbids` provides a consistent mapping of identifiers and names across these different types. See the package vignette for more details on the data model: TODO: INSERT LINK TO VIGNETTE LATER + +This package is a product of Teal Insights and not sponsored by or affiliated with the World Bank in any way, except for the use of the World Bank IDS API. + +## Installation + +You can install the development version of `wbids` like this: + +```r +pak::pak("teal-insights/r-wbids") +``` + +## Usage + +The main function `ids_get()` provides an interface to download multiple IDS series for multiple geographies and counterparts and specific date ranges. + +```r +library(wbids) + +ids_get( + geographies = c("ZMB", "ZAF"), + series = c("DT.DOD.DPPG.CD", "BM.GSR.TOTL.CD"), + counterparts = c("216", "231"), + start_date = 2015, + end_date = 2020 +) +``` + +The package comes with prepared metadata about available series, geographies, counterparts, and topics: + +```r +ids_list_series() +ids_list_georaphies() +ids_list_counterparts() +ids_list_series_topics() +``` + +This data can be used to enrich the IDS data or facilitate data discovery. For further applications, please consult [Teal Insights’ Guide to Working with the World Bank International Debt Statistics](https://teal-insights.github.io/teal_insights_guide_to_wbids/). + +The interface and namings are fully consistent with World Development Indicators (WDI) data provided through the 'wbwdi' package. You can find details on [github.com/tidy-intelligence/r-wbwdi](https://github.com/tidy-intelligence/r-wbwdi). diff --git a/README.md b/README.md index 592d1fa..ebfe3bd 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ [![R-CMD-check](https://github.com/Teal-Insights/r-wbids/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/Teal-Insights/r-wbids/actions/workflows/R-CMD-check.yaml) +[![Codecov test +coverage](https://codecov.io/gh/Teal-Insights/r-wbids/graph/badge.svg)](https://app.codecov.io/gh/Teal-Insights/r-wbids) `wbids` is an R package that provides a modern, flexible interface for diff --git a/data-raw/wbids-data.R b/data-raw/wbids-data.R index 6a16de3..575a0bc 100644 --- a/data-raw/wbids-data.R +++ b/data-raw/wbids-data.R @@ -16,25 +16,54 @@ url_geographies_wdi <- paste0( geographies_raw <- request(url_geographies_wdi) |> req_perform() |> - resp_body_json() - -aggregates <- geographies_raw[[2]] |> - bind_rows() |> - unnest(region) |> - filter(region == "Aggregates") |> - pull(id) + resp_body_json(simplifyVector = TRUE) geographies_wdi <- geographies_raw[[2]] |> - bind_rows() |> - select( - geography_iso3code = id, - geography_iso2code = iso2Code, - geography_name = name + as_tibble() |> + rename( + geography_id = "id", + geography_iso2code = "iso2Code", + geography_name = "name" + ) |> + unnest_wider("region") |> + rename( + region_id = "id", + region_iso2code = "iso2code", + region_name = "value" + ) |> + unnest_wider("adminregion") |> + rename( + admin_region_id = "id", + admin_region_iso2code = "iso2code", + admin_region_name = "value" + ) |> + unnest_wider("incomeLevel") |> + rename( + income_level_id = "id", + income_level_iso2code = "iso2code", + income_level_name = "value" + ) |> + unnest_wider("lendingType") |> + rename( + lending_type_id = "id", + lending_type_iso2code = "iso2code", + lending_type_name = "value", + capital_city = "capitalCity" + ) |> + mutate( + longitude = if_else( + .data$longitude == "", NA_real_, as.numeric(.data$longitude) + ), + latitude = if_else( + .data$latitude == "", NA_real_, as.numeric(.data$latitude) + ), + geography_type = if_else( + .data$region_name == "Aggregates", "Region", "Country" + ), + across(where(is.character), ~ if_else(.x == "", NA, .x)), + across(where(is.character), trimws) ) |> - distinct() |> - mutate(geography_type = if_else( - geography_iso3code %in% aggregates, "Region", "Country" - )) + relocate(c("geography_type", "capital_city"), .after = "geography_iso2code") # Fetch counterparts from World Bank International Debt Statistics API ---- @@ -196,7 +225,7 @@ counterparts_ids_enriched <- counterparts_ids |> counterparts_ids_cleaned <- counterparts_ids_enriched |> left_join( geographies_wdi, - join_by(geography_iso2code, geography_iso3code, geography_type) + join_by(geography_iso2code, geography_id, geography_type) ) |> mutate(counterpart_name = if_else( !is.na(geography_name), geography_name, counterpart_name @@ -233,19 +262,33 @@ counterparts <- counterparts_ids_cleaned |> .default = "Other" ) ) |> - rename(geography_id = geography_iso3code) + select( + counterpart_id, + counterpart_name, + counterpart_iso2code = geography_iso3code, + counterpart_iso3code = geography_id, + counterpart_type + ) # Use processed counterparts to enrich geographies ----------------------- -geographies <- geographies_wdi |> - rename(geography_id = geography_iso3code) |> - bind_rows( - counterparts |> - filter(!is.na(geography_id)) |> - select(contains("geography"), geography_name = counterpart_name) - ) |> - distinct() |> - arrange(geography_id) +url_geographies_ids <- paste0( + "https://api.worldbank.org/v2/sources/", + "6/country?per_page=32500&format=json" +) + +geographies_ids_raw <- request(url_geographies_ids) |> + req_perform() |> + resp_body_json() + +geographies <- geographies_ids_raw$source[[1]]$concept[[1]]$variable |> + bind_rows() |> + select(geography_id = id) |> + left_join( + geographies <- geographies_wdi |> + select(-c(longitude, latitude)), + join_by(geography_id) + ) # Prepare series data ---------------------------------------------------------- diff --git a/man/ids_list_geographies.Rd b/man/ids_list_geographies.Rd index f9044c7..97a7c9d 100644 --- a/man/ids_list_geographies.Rd +++ b/man/ids_list_geographies.Rd @@ -10,7 +10,8 @@ ids_list_geographies() \value{ A tibble containing the available geographies and their attributes: \describe{ -\item{geography_id}{The unique identifier for the geography (e.g., "ZMB").} +\item{geography_id}{ISO 3166-1 alpha-3 code of the geography (e.g., +"ZMB").} \item{geography_name}{The standardized name of the geography (e.g., "Zambia").} \item{geography_iso2code}{ISO 3166-1 alpha-2 code of the geography (e.g., diff --git a/man/prompt_user.Rd b/man/prompt_user.Rd new file mode 100644 index 0000000..61d3dc7 --- /dev/null +++ b/man/prompt_user.Rd @@ -0,0 +1,13 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/ids_bulk.R +\name{prompt_user} +\alias{prompt_user} +\title{Prompt a user with a question} +\usage{ +prompt_user(prompt) +} +\description{ +Wrapper around base::readline to facilitate testing. Cannot be tested +because of the base binding. +} +\keyword{internal} diff --git a/man/wbids-package.Rd b/man/wbids-package.Rd index e9119db..a1d304c 100644 --- a/man/wbids-package.Rd +++ b/man/wbids-package.Rd @@ -13,7 +13,7 @@ Useful links: \itemize{ \item \url{https://teal-insights.github.io/r-wbids} \item \url{https://github.com/teal-insights/r-wbids} - \item Report bugs at \url{https://github.com/Teal-Insights/r-wbids/issues} + \item Report bugs at \url{https://github.com/teal-insights/r-wbids/issues} } } diff --git a/tests/testthat/test-ids_bulk.R b/tests/testthat/test-ids_bulk.R index 96cea4d..7d0080e 100644 --- a/tests/testthat/test-ids_bulk.R +++ b/tests/testthat/test-ids_bulk.R @@ -30,7 +30,6 @@ test_that("ids_bulk handles custom file paths", { test_url, file_path = temp_path, quiet = TRUE, warn_size = FALSE ) - # Check that file is cleaned up when we're done expect_false(file.exists(temp_path)) }) @@ -52,10 +51,9 @@ test_that("ids_bulk requires readxl package", { ) }) -test_that("quiet parameter controls message output", { +test_that("ids_bulk handles message parameter correctly", { test_url <- ids_bulk_files()$file_url[1] - # Create a small mock dataset that read_excel would return mock_data <- tibble::tibble( "Country Code" = "ABC", "Country Name" = "Test Country", @@ -65,7 +63,6 @@ test_that("quiet parameter controls message output", { "2020" = 100 ) - # Set up mocked bindings local_mocked_bindings( download_file = function(...) TRUE ) @@ -80,7 +77,6 @@ test_that("quiet parameter controls message output", { check_interactive = function() FALSE ) - # Should show messages expect_message( ids_bulk(test_url, quiet = FALSE, warn_size = FALSE), "Downloading file" @@ -93,8 +89,6 @@ test_that("quiet parameter controls message output", { ids_bulk(test_url, quiet = FALSE, warn_size = FALSE), "Processing file" ) - - # Should not show messages expect_no_message( ids_bulk(test_url, quiet = TRUE, warn_size = FALSE) ) @@ -104,15 +98,12 @@ test_that("ids_bulk handles timeout parameter correctly", { skip_if_offline() skip_on_cran() - # Mock a slow URL that will definitely timeout mock_url <- "http://httpbin.org/delay/10" - # Mock interactive to return FALSE local_mocked_bindings( check_interactive = function() FALSE ) - # Test with short timeout (1 second) expect_warning( expect_error( ids_bulk(mock_url, timeout = 1, warn_size = FALSE), @@ -126,25 +117,20 @@ test_that("ids_bulk handles warn_size parameter", { skip_if_offline() skip_on_cran() - # Get a real file URL to test with test_url <- ids_bulk_files()$file_url[1] - # Mock download_file with mocked_bindings local_mocked_bindings( download_file = function(...) TRUE ) - # Mock validate_file with mocked_bindings local_mocked_bindings( validate_file = function(...) TRUE ) - # Mock interactive to return FALSE local_mocked_bindings( check_interactive = function() FALSE ) - # Should show warning with warn_size = TRUE expect_warning( download_bulk_file( test_url, tempfile(), 60, warn_size = TRUE, quiet = TRUE @@ -153,7 +139,6 @@ test_that("ids_bulk handles warn_size parameter", { fixed = FALSE ) - # Should not show warning with warn_size = FALSE expect_no_warning( download_bulk_file( test_url, tempfile(), 60, warn_size = FALSE, quiet = TRUE @@ -162,7 +147,6 @@ test_that("ids_bulk handles warn_size parameter", { }) test_that("ids_bulk validates downloaded files", { - # Mock an empty file temp_file <- tempfile() file.create(temp_file) @@ -171,7 +155,6 @@ test_that("ids_bulk validates downloaded files", { "Download failed: Empty file" ) - # Mock a non-existent file expect_error( validate_file("nonexistent.xlsx"), "Download failed: File not created" @@ -182,16 +165,13 @@ test_that("download_bulk_file downloads files correctly", { skip_if_offline() skip_on_cran() - # Get a real file URL to test with test_url <- ids_bulk_files()$file_url[1] test_path <- tempfile(fileext = ".xlsx") - # Mock interactive check to avoid prompts local_mocked_bindings( check_interactive = function() FALSE ) - # Test successful download withr::with_options( list(timeout = 300), expect_no_error( @@ -205,16 +185,13 @@ test_that("download_bulk_file downloads files correctly", { ) ) - # Verify file exists and has content expect_true(file.exists(test_path)) expect_gt(file.size(test_path), 0) - # Clean up unlink(test_path) }) test_that("read_bulk_file reads files correctly", { - # Loading the sample file is slow, so skip on CRAN skip_on_cran() test_path <- test_path("data/sample.xlsx") @@ -226,24 +203,20 @@ test_that("process_bulk_data processes data correctly", { test_path <- test_path("data/sample.rds") result <- process_bulk_data(readRDS(test_path)) - # Check structure expect_s3_class(result, "tbl_df") expect_named( result, c("geography_id", "series_id", "counterpart_id", "year", "value") ) - # Check data types expect_type(result$geography_id, "character") expect_type(result$series_id, "character") expect_type(result$counterpart_id, "character") expect_type(result$year, "integer") expect_type(result$value, "double") - # Check for non-empty result expect_gt(nrow(result), 0) - # Check that all values in required columns are non-NA expect_false(any(is.na(result$geography_id))) expect_false(any(is.na(result$series_id))) expect_false(any(is.na(result$counterpart_id))) @@ -254,31 +227,88 @@ test_that("ids_bulk downloads and processes data correctly", { skip_if_offline() skip_on_cran() - # Get a real file URL to test with - test_url <- ids_bulk_files()$file_url[1] + test_url <- paste0( + "https://datacatalogfiles.worldbank.org/ddh-published/0038015/DR0092201/", + "A_D.xlsx?versionId=2024-10-08T01:35:39.3946879Z" + ) test_path <- tempfile(fileext = ".xlsx") - # Mock slow-running functions local_mocked_bindings( check_interactive = function() FALSE, download_bulk_file = function(...) TRUE, read_bulk_file = function(...) readRDS(test_path("data/sample.rds")) ) - # Check that output is a tibble and has expected column names and types result <- ids_bulk( test_url, file_path = test_path, quiet = TRUE, warn_size = FALSE ) expect_s3_class(result, "tbl_df") - expected_colnames <- c( + expected_columns <- c( "geography_id", "series_id", "counterpart_id", "year", "value" ) - expect_equal(colnames(result), expected_colnames) + expect_equal(colnames(result), expected_columns) - expected_coltypes <- c( + expected_types <- c( "character", "character", "character", "integer", "numeric" ) - expect_true(all(lapply(result, class) == expected_coltypes)) + expect_true(all(lapply(result, class) == expected_types)) +}) + +test_that("check_interactive returns expected results", { + if (interactive()) { + expect_true(check_interactive()) + } else { + expect_false(check_interactive()) + } +}) + +test_that("download_file downloads a file correctly", { + url <- "https://example.com" + destfile <- tempfile(fileext = ".txt") + if (file.exists(destfile)) { + file.remove(destfile) + } + download_file(url, destfile, quiet = TRUE) + expect_true(file.exists(destfile)) + file.remove(destfile) +}) + +test_that("warn_size warning is triggered & user prompt is handled correctly", { + + test_url <- paste0( + "https://datacatalogfiles.worldbank.org/ddh-published/0038015/DR0092201/", + "A_D.xlsx?versionId=2024-10-08T01:35:39.3946879Z" + ) + + with_mocked_bindings( + get_response_headers = function(...) list(`content-length` = 150 * 1024^2), + check_interactive = function(...) TRUE, + prompt_user = function(...) "y", + { + expect_warning( + download_bulk_file( + test_url, tempfile(fileext = ".xlsx"), + timeout = 30, warn_size = TRUE, quiet = TRUE + ), + regexp = "may take several minutes to download." + ) + } + ) + + with_mocked_bindings( + get_response_headers = function(...) list(`content-length` = 150 * 1024^2), + check_interactive = function(...) TRUE, + prompt_user = function(...) "n", + { + expect_error( + suppressWarnings(download_bulk_file( + test_url, tempfile(fileext = ".xlsx"), + timeout = 30, warn_size = TRUE, quiet = TRUE + )), + regexp = "Download cancelled." + ) + } + ) }) diff --git a/tests/testthat/test-ids_bulk_files.R b/tests/testthat/test-ids_bulk_files.R new file mode 100644 index 0000000..701b4a9 --- /dev/null +++ b/tests/testthat/test-ids_bulk_files.R @@ -0,0 +1,6 @@ +test_that("ids_bulk_files returns a tibble with expected columns", { + result <- ids_bulk_files() + expected_columns <- c("file_name", "file_url", "last_updated_date") + expect_equal(colnames(result), expected_columns) + expect_s3_class(result, "tbl_df") +}) diff --git a/tests/testthat/test-ids_bulk_series.R b/tests/testthat/test-ids_bulk_series.R new file mode 100644 index 0000000..2f7db8f --- /dev/null +++ b/tests/testthat/test-ids_bulk_series.R @@ -0,0 +1,9 @@ +test_that("ids_bulk_series returns a tibble with expected columns", { + result <- ids_bulk_series() + expected_columns <- c( + "series_id", "series_name", + "source_id", "source_name", "source_note", "source_organization" + ) + expect_equal(colnames(result), expected_columns) + expect_s3_class(result, "tbl_df") +}) diff --git a/tests/testthat/test-ids_get.R b/tests/testthat/test-ids_get.R index 8e2fe34..f7ae58a 100644 --- a/tests/testthat/test-ids_get.R +++ b/tests/testthat/test-ids_get.R @@ -1,61 +1,225 @@ -test_that("geographies input validation works", { - expect_error( - ids_get(geographies = NA, series = "DT.DOD.DPPG.CD", counterparts = "all") +test_that("ids_get returns a tibble with expected columns", { + result <- ids_get( + geographies = "ZMB", + series = "DT.DOD.DPPG.CD", + counterparts = c("216"), + start_date = 2015, + end_date = 2016, + progress = FALSE + ) + expect_s3_class(result, "tbl_df") + expect_true(nrow(result) > 0) + expected_columns <- c( + "geography_id", "series_id", "counterpart_id", "year", "value" ) + expect_equal(colnames(result), expected_columns) +}) + +test_that("ids_get returns a large data", { + result <- ids_get( + geographies = "ZMB", + series = "DT.DOD.DPPG.CD", + counterparts = c("all") + ) + expect_s3_class(result, "tbl_df") + expect_true(nrow(result) > 0) + expected_columns <- c( + "geography_id", "series_id", "counterpart_id", "year", "value" + ) + expect_equal(colnames(result), expected_columns) +}) + +test_that("ids_get handels invalid geography input", { expect_error( - ids_get(geographies = 123, series = "DT.DOD.DPPG.CD", counterparts = "all") + ids_get( + geographies = NA, + series = "DT.DOD.DPPG.CD" + ), + "`geographies` must be a character vector and cannot contain NA values" ) }) -test_that("series input validation works", { +test_that("ids_get handels invalid series input", { expect_error( - ids_get(geographies = "ZMB", series = NA, counterparts = "all") + ids_get( + geographies = "ZMB", + series = NA + ), + "`series` must be a character vector and cannot contain NA values" ) +}) + +test_that("ids_get handels invalid progress input", { expect_error( - ids_get(geographies = "ZMB", series = 123, counterparts = "all") + ids_get( + geographies = "ZMB", series = "DT.DOD.DPPG.CD", progress = "yes" + ), + "`progress` must be either TRUE or FALSE." + ) +}) + +test_that("ids_get handels valid progress input", { + expect_silent( + ids_get( + geographies = "ZMB", series = "DT.DOD.DPPG.CD", counterparts = "265", + start_date = 2015, end_date = 2016, + progress = TRUE + ) ) }) -test_that("counterparts input validation works", { +test_that("validate_character_vector correctly validates character vectors", { + expect_error( + validate_character_vector(NA, "geographies"), + "`geographies` must be a character vector and cannot contain NA values" + ) expect_error( - ids_get(geographies = "ZMB", series = "DT.DOD.DPPG.CD", counterparts = NA) + validate_character_vector(c("ZMB", NA), "series"), + "`series` must be a character vector and cannot contain NA values" ) expect_error( - ids_get(geographies = "ZMB", series = "DT.DOD.DPPG.CD", counterparts = 123) + validate_character_vector(123, "geographies"), + "`geographies` must be a character vector and cannot contain NA values" ) + expect_silent(validate_character_vector(c("ZMB", "CHN"), "geographies")) }) -test_that("start_date and end_date input validation works", { +test_that("validate_date correctly validates date values", { expect_error( - ids_get(geographies = "ZMB", series = "DT.DOD.DPPG.CD", - counterparts = "all", start_date = 1960) + validate_date(1969, "start_date"), + "`start_date` must be a single numeric value representing a year >= 1970" ) expect_error( - ids_get(geographies = "ZMB", series = "DT.DOD.DPPG.CD", - counterparts = "all", end_date = 1960) + validate_date("2020", "end_date"), + "`end_date` must be a single numeric value representing a year >= 1970" ) + expect_silent(validate_date(1970, "start_date")) + expect_silent(validate_date(2020, "end_date")) + expect_silent(validate_date(NULL, "end_date")) + expect_silent(validate_date(NULL, "start_date")) + expect_equal(create_time(NULL, NULL), "all") +}) + +test_that("validate_progress checks logical values for progress", { + expect_error(validate_progress("yes"), + "`progress` must be either TRUE or FALSE") + expect_silent(validate_progress(TRUE)) + expect_silent(validate_progress(FALSE)) +}) + +test_that("create_time generates correct time sequence", { + expect_equal(create_time(2015, 2017), c("YR2015", "YR2016", "YR2017")) + expect_equal(create_time(1970, 1970), "YR1970") + expect_equal(create_time(NULL, NULL), "all") expect_error( - ids_get(geographies = "ZMB", series = "DT.DOD.DPPG.CD", - counterparts = "all", start_date = 2020, end_date = 2015) + create_time(2020, 2019), + "`start_date` cannot be greater than `end_date`" ) }) -test_that("progress input validation works", { - expect_error( - ids_get(geographies = "ZMB", series = "DT.DOD.DPPG.CD", - counterparts = "all", progress = "yes") +test_that("get_debt_statistics returns correctly structured tibble", { + mock_perform_request <- list( + list(variable = list( + list(concept = "Country", id = "ZMB"), + list(concept = "Series", id = "DT.DOD.DPPG.CD"), + list(concept = "Counterpart-Area", id = "216"), + list(concept = "Time", value = "2020") + ), value = 100), + list(variable = list( + list(concept = "Country", id = "ZMB"), + list(concept = "Series", id = "DT.DOD.DPPG.CD"), + list(concept = "Counterpart-Area", id = "216"), + list(concept = "Time", value = "2021") + ), value = 200) + ) + + with_mocked_bindings( + perform_request = function(...) mock_perform_request, + { + result <- get_debt_statistics( + geography = "ZMB", + series = "DT.DOD.DPPG.CD", + counterpart = "216", + time = "YR2020", + progress = FALSE + ) + + expect_s3_class(result, "tbl_df") + expected_columns <- c( + "geography_id", "series_id", "counterpart_id", "year", "value" + ) + expect_equal(colnames(result), expected_columns) + expect_equal(nrow(result), 2) + expect_equal(result$geography_id, c("ZMB", "ZMB")) + expect_equal(result$series_id, c("DT.DOD.DPPG.CD", "DT.DOD.DPPG.CD")) + expect_equal(result$counterpart_id, c("216", "216")) + expect_equal(result$year, c(2020, 2021)) + expect_equal(result$value, c(100, 200)) + } ) }) -test_that("time range construction works", { - result <- ids_get(geographies = "ZMB", series = "DT.DOD.DPPG.CD", - counterparts = "all", start_date = 2015, end_date = 2020) - expect_true(all(unique(result$year) == seq(2015, 2020, 1))) +test_that("ids_get handles empty data gracefully", { + + mock_data <- list( + list( + "variable" = list( + list( + "concept" = character(), + "id" = character(), + "value" = character() + ), + list( + "concept" = character(), + "id" = character(), + "value" = character() + ), + list( + "concept" = character(), + "id" = character(), + "value" = character() + ), + list( + "concept" = character(), + "id" = character(), + "value" = character() + ) + ), + "value" = numeric() + ) + ) + + with_mocked_bindings( + perform_request = function(...) mock_data, + { + result <- ids_get("ZMB", "DT.DOD.DPPG.CD") + expect_equal(nrow(result), 0) + } + ) }) -test_that("API request behavior without errors", { - result <- ids_get(geographies = "ZMB", series = "DT.DOD.DPPG.CD", - counterparts = "all") - expect_type(result, "list") - expect_true(nrow(result) > 0) +test_that("ids_get handles empty or incomplete data gracefully", { + incomplete_data_mock <- list( + list( + "variable" = list( + list("concept" = "Country", "id" = "ZMB"), + list("concept" = "Series", "id" = NA), + list("concept" = "Counterpart-Area", "id" = "all"), + list("concept" = "Time", "value" = "2020") + ), + "value" = NULL + ) + ) + + with_mocked_bindings( + perform_request = function(...) incomplete_data_mock, + { + result <- get_debt_statistics( + "ZMB", "DT.DOD.DPPG.CD", "all", "YR2020", FALSE + ) + expect_equal(nrow(result), 1) + expect_true(is.na(result$series_id[1])) + expect_equal(result$value, NA_real_) + } + ) }) diff --git a/tests/testthat/test-ids_list_counterparts.R b/tests/testthat/test-ids_list_counterparts.R new file mode 100644 index 0000000..5632f95 --- /dev/null +++ b/tests/testthat/test-ids_list_counterparts.R @@ -0,0 +1,10 @@ +test_that("ids_list_counterparts returns a tibble with expected columns", { + result <- ids_list_counterparts() + expected_columns <- c( + "counterpart_id", "counterpart_name", + "counterpart_iso2code", "counterpart_iso3code", + "counterpart_type" + ) + expect_equal(colnames(result), expected_columns) + expect_s3_class(result, "tbl_df") +}) diff --git a/tests/testthat/test-ids_list_geographies.R b/tests/testthat/test-ids_list_geographies.R new file mode 100644 index 0000000..449cc65 --- /dev/null +++ b/tests/testthat/test-ids_list_geographies.R @@ -0,0 +1,12 @@ +test_that("ids_list_geographies returns a tibble with expected columns", { + result <- ids_list_geographies() + expect_s3_class(result, "tbl_df") + expected_columns <- c( + "geography_id", "geography_iso2code", "geography_type", "capital_city", + "geography_name", "region_id", "region_iso2code", "region_name", + "admin_region_id", "admin_region_iso2code", "admin_region_name", + "income_level_id", "income_level_iso2code", "income_level_name", + "lending_type_id", "lending_type_iso2code", "lending_type_name" + ) + expect_equal(colnames(result), expected_columns) +}) diff --git a/tests/testthat/test-ids_list_series.R b/tests/testthat/test-ids_list_series.R new file mode 100644 index 0000000..fa963f9 --- /dev/null +++ b/tests/testthat/test-ids_list_series.R @@ -0,0 +1,9 @@ +test_that("ids_list_series returns a tibble with expected columns", { + result <- ids_list_series() + expect_s3_class(result, "tbl_df") + expected_columns <- c( + "series_id", "series_name", + "source_id", "source_name", "source_note", "source_organization" + ) + expect_equal(colnames(result), expected_columns) +}) diff --git a/tests/testthat/test-ids_list_series_topics.R b/tests/testthat/test-ids_list_series_topics.R new file mode 100644 index 0000000..c2151a4 --- /dev/null +++ b/tests/testthat/test-ids_list_series_topics.R @@ -0,0 +1,8 @@ +test_that("ids_list_series_topics returns a tibbe with expected columns", { + result <- ids_list_series_topics() + expect_s3_class(result, "tbl_df") + expected_columns <- c( + "series_id", "topic_id", "topic_name" + ) + expect_equal(colnames(result), expected_columns) +}) diff --git a/tests/testthat/test-perform_request.R b/tests/testthat/test-perform_request.R index 596805e..a82c0cd 100644 --- a/tests/testthat/test-perform_request.R +++ b/tests/testthat/test-perform_request.R @@ -1,6 +1,82 @@ -test_that("perform_request returns data for a series resource", { - resource <- "series" - result <- perform_request(resource) - expect_true(result[[1]]$name == "International Debt Statistics") - expect_true(result[[1]]$id == "6") -}) +test_that("perform_request handles error responses", { + mock_error_response <- list( + list( + message = list( + list(id = "120", value = "Invalid indicator") + ) + ) + ) + + with_mocked_bindings( + req_perform = function(...) mock_error_response, + is_request_error = function(...) TRUE, + handle_request_error = function(resp) stop("API error: Invalid indicator"), + { + expect_error(perform_request("indicators"), + "API error: Invalid indicator") + } + ) +}) + +test_that("perform_request validates per_page parameter", { + expect_error(perform_request("series", per_page = 50000)) + expect_silent(perform_request("series", per_page = 1000)) +}) + +test_that("validate_per_page handles valid per_page values", { + expect_silent(validate_per_page(1000)) + expect_silent(validate_per_page(1)) + expect_silent(validate_per_page(32500)) +}) + +test_that("validate_per_page throws an error for invalid per_page values", { + expect_error(validate_per_page(0), + "must be an integer between 1 and 32,500") + expect_error(validate_per_page(32501), + "must be an integer between 1 and 32,500") + expect_error(validate_per_page("1000"), + "must be an integer between 1 and 32,500") + expect_error(validate_per_page(1000.5), + "must be an integer between 1 and 32,500") +}) + +test_that("create_request constructs a request with default parameters", { + req <- create_request( + "https://api.worldbank.org/v2/sources/6/", "series", 1000 + ) + expect_equal( + req$url, + "https://api.worldbank.org/v2/sources/6/series?format=json&per_page=1000" + ) +}) + +test_that("is_request_error identifies error responses correctly", { + mock_resp <- structure(list(status_code = 400), class = "httr2_response") + expect_true(is_request_error(mock_resp)) +}) + +test_that("perform_request handles API errors gracefully", { + expect_error(perform_request("nonexistent"), "HTTP 400 Bad Request.") +}) + +test_that("perform_request handles wrong requests gracefully", { + + mocked_request <- request(paste0( + "https://api.worldbank.org/v2/sources/6/", + "country/ZMB/series/DT.DOD.DPPG.CD/counterpart-area/XXX/time/all", + "?format=json&per_page=1000" + )) + + with_mocked_bindings( + create_request = function(...) mocked_request, + { + expect_error( + perform_request("country"), + paste0( + "API Error Code 160 : Data not found. ", + "The provided parameter value is not valid or data not found." + ) + ) + } + ) +}) diff --git a/tests/testthat/test-read_bulk_info.R b/tests/testthat/test-read_bulk_info.R new file mode 100644 index 0000000..9ae9f42 --- /dev/null +++ b/tests/testthat/test-read_bulk_info.R @@ -0,0 +1,5 @@ +test_that("read_bulk_info returns necessary data structure", { + result <- read_bulk_info() + expect_type(result, "list") + expect_true(all(c("indicators", "resources") %in% names(result))) +}) diff --git a/vignettes/data-model.qmd b/vignettes/data-model.qmd index a1c5df7..7944c9b 100644 --- a/vignettes/data-model.qmd +++ b/vignettes/data-model.qmd @@ -69,7 +69,7 @@ erDiagram | Column name | Description | Example value | |-----------------|-------------------------------------|-----------------| -| geography_id | Unique identifier for the geography | ZMB | +| geography_id | ISO 3166-1 alpha-3 code of the geography | ZMB | | geography_name | Standardized name of the geography | Zambia | | geography_iso2code | ISO 3166-1 alpha-2 code of the geography | ZM | | geography_type | Type of geography (e.g., country, region) | Country |