diff --git a/.Rbuildignore b/.Rbuildignore index e494ae9..c05e3bc 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -1,4 +1,5 @@ ^renv$ +^\.[Rr]environ$ ^renv\.lock$ ^.*\.Rproj$ ^\.Rproj\.user$ @@ -18,3 +19,4 @@ ^doc$ ^Meta$ ^AGENTS\.md$ +^CRAN-SUBMISSION$ diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index 3bab553..8c372cd 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -67,7 +67,6 @@ jobs: with: extra-packages: | any::rcmdcheck - github::OHDSI/ROhdsiWebApi needs: check - uses: r-lib/actions/check-r-package@v2 diff --git a/.github/workflows/pkgdown.yaml b/.github/workflows/pkgdown.yaml index 33a65fe..b301fd5 100644 --- a/.github/workflows/pkgdown.yaml +++ b/.github/workflows/pkgdown.yaml @@ -33,7 +33,6 @@ jobs: with: extra-packages: | any::pkgdown - github::OHDSI/ROhdsiWebApi local::. needs: website diff --git a/.github/workflows/test-coverage.yaml b/.github/workflows/test-coverage.yaml new file mode 100644 index 0000000..0a6579f --- /dev/null +++ b/.github/workflows/test-coverage.yaml @@ -0,0 +1,64 @@ +# 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, develop] + pull_request: + +name: test-coverage.yaml + +permissions: read-all + +jobs: + test-coverage: + runs-on: ubuntu-latest + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + # Use the CI library for coverage and its test subprocesses. + RENV_CONFIG_AUTOLOADER_ENABLED: "false" + + steps: + - uses: actions/checkout@v6 + + - uses: r-lib/actions/setup-r@v2 + + - 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") + ) + print(cov) + covr::to_cobertura(cov) + shell: Rscript {0} + + - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7 + 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 }} + files: ./cobertura.xml + plugins: 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@v7 + with: + name: coverage-test-failures + path: ${{ runner.temp }}/package diff --git a/.gitignore b/.gitignore index 149f374..6d53883 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ .Rhistory .RData .Ruserdata +.Renviron +.renviron .positai .DS_Store # Generated pkgdown site. The published copy is maintained on the gh-pages branch. diff --git a/DESCRIPTION b/DESCRIPTION index 30a4b78..5bbccf4 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: studyGenerics Type: Package Title: A Centralised Repository of Tested and Frequently Used Methods for OMOP-CDM Studies -Version: 0.1.3 +Version: 0.1.4 Authors@R: c( person( "Cesar", "Barboza", @@ -21,12 +21,22 @@ Authors@R: c( person( "Erasmus MC", role = "cph" + ), + person("Gowtham", "Rao", role = "ctb", + comment = "ROhdsiWebApi retrieval helpers"), + person("Martijn", "Schuemie", role = "ctb", + comment = "ROhdsiWebApi retrieval helpers"), + person("Jamie", "Gilbert", role = "ctb", + comment = "ROhdsiWebApi retrieval helpers"), + person( + "Observational Health Data Sciences and Informatics", + role = "cph", + comment = "Bundled ROhdsiWebApi retrieval helpers (Apache-2.0)" ) ) Description: Provides generic functions for routine tasks required to run OMOP-CDM studies. - In addition to testing these recurring methods, studyGenerics aims to centralise - collaboration and speed up the development of these types of projects. The optional - ROhdsiWebApi package is available from . + In addition to testing these recurring methods, the package aims to centralise + collaboration and speed up the development of these types of projects. License: Apache License (>= 2) Encoding: UTF-8 LazyData: true @@ -49,13 +59,16 @@ Suggests: desc, gert, gh, + httr (>= 1.3.1), jsonvalidate, knitr, + lubridate, omopgenerics, purrr, rmarkdown, - ROhdsiWebApi, - testthat (>= 3.0.0) + RJSONIO, + rlang, + testthat (>= 3.2.0) Config/testthat/edition: 3 URL: https://mi-erasmusmc.github.io/studyGenerics/, https://github.com/mi-erasmusmc/studyGenerics VignetteBuilder: knitr diff --git a/NAMESPACE b/NAMESPACE index 0b9888d..382212a 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -59,7 +59,7 @@ importFrom(stringr, str_replace_all, str_to_lower ) -importFrom(usethis,proj_set) +importFrom(usethis,proj_get) importFrom(zip, unzip, zip diff --git a/NEWS.md b/NEWS.md index 0a107dc..8be154c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,9 @@ +# studyGenerics 0.1.4 + +* `pullConceptsAtlas()` now integrates required functions from ROhdsiWebApi in order to submit to CRAN. + +* Dependency on R >= 4.2.0 added to DESCRIPTION. + # studyGenerics 0.1.3 * Initial CRAN release. diff --git a/R/pullConceptsAtlas.R b/R/pullConceptsAtlas.R index 1bd86f9..053b1d9 100644 --- a/R/pullConceptsAtlas.R +++ b/R/pullConceptsAtlas.R @@ -2,7 +2,15 @@ #' #' Retrieves each concept set definition from the ATLAS WebAPI, cleans the #' concept set name for use as a file name and writes the JSON definition to -#' `inst/concept_sets//`. +#' `inst/concept_sets//`. +#' +#' @details +#' Requires the optional CRAN packages `httr`, `RJSONIO`, `lubridate`, and `rlang`. +#' For a protected WebAPI, set `ATLAS_TOKEN` in your local `.Renviron` file and +#' restart R. The token may include the `Bearer ` prefix. It is sent only to +#' the supplied WebAPI. Do not commit the token to version control. +#' The internal retrieval helpers are adapted from OHDSI's `ROhdsiWebApi` +#' under the Apache License 2.0; see `inst/COPYRIGHTS` in the package source. #' #' @param conceptSetList A numeric vector of ATLAS concept set IDs to download. #' @param conceptSetType A character string used to group the downloaded concept @@ -11,10 +19,12 @@ #' @param deletePrevious A logical value indicating whether to delete existing files in #' the target `` folder before downloading the new concept sets. #' @param patternToRemove A vector of regular expressions containing the pattern(s) -#' to be removed from the ATLAS concept set names before creating the output JSON files. +#' to be removed from the ATLAS concept set names before creating the output JSON files. +#' @param authHeader Authorization header for the WebAPI. Defaults to +#' `Sys.getenv("ATLAS_TOKEN")`. Use `""` for a public WebAPI. #' #' @returns A tibble with one row per downloaded concept set and the columns -#' `conceptSetId`, `originalName`, `cleanName`, and `jsonPath`. +#' `conceptSetId`, `originalName`, `cleanName`, and `fileName`. #' #' @importFrom checkmate assertCharacter assertDouble #' @importFrom here here @@ -45,7 +55,8 @@ pullConceptsAtlas <- function( conceptSetType, baseUrl = "https://atlas.darwin-eu.org/WebAPI", deletePrevious = TRUE, - patternToRemove = c("p[0-9]{1}_c[0-9]{1}_[0-9]{3}_") + patternToRemove = c("p[0-9]{1}_c[0-9]{1}_[0-9]{3}_"), + authHeader = Sys.getenv("ATLAS_TOKEN") ) { checkmate::assertDouble(conceptSetList) @@ -53,7 +64,10 @@ pullConceptsAtlas <- function( checkmate::assertCharacter(baseUrl) checkmate::assertLogical(deletePrevious) checkmate::assertCharacter(patternToRemove) - requireInstall("ROhdsiWebApi") + for (package in c("httr", "RJSONIO", "lubridate", "rlang")) { + requireInstall(package) + } + checkmate::assertString(authHeader) # --- Extract IDs --- ParallelLogger::logInfo(glue::glue("Extracting ids from excel file")) @@ -93,9 +107,10 @@ pullConceptsAtlas <- function( ) # Fetch definition - conceptSet <- ROhdsiWebApi::getConceptSetDefinition( - conceptSetId = id, - baseUrl = baseUrl + conceptSet <- getConceptSetDefinition( + conceptSetId = id, + baseUrl = baseUrl, + authHeader = authHeader ) # Clean concept set name @@ -165,5 +180,184 @@ pullConceptsAtlas <- function( results_df <- dplyr::bind_rows( results_list ) - return(results_df) -} + return(results_df) +} + +# Adapted from OHDSI/ROhdsiWebApi, commit: +# 5911ab0973ee18c5e72b62c6b999dc1f1bb21e79 +# https://github.com/OHDSI/ROhdsiWebApi/tree/5911ab0973ee18c5e72b62c6b999dc1f1bb21e79/R +# Sources: AutoGeneratedDefinitions.R, GetPostDeleteDefinition.R, Private.R, +# WebApi.R, httrWrappers.R. See inst/COPYRIGHTS for the function/source mapping. +# +# Copyright 2022 Observational Health Data Sciences and Informatics +# +# The original files are part of ROhdsiWebApi (AutoGeneratedDefinitions.R +# carries the upstream notice "This file is part of FeatureExtraction"). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Modified for studyGenerics: internal helpers limited to concept-set retrieval; +# conditional dependencies; explicit request-scoped authentication instead of a +# cached package environment; authenticated version checks; request timeouts; +# no redirects; input/response checks; standard HTTP errors instead of metadata +# lookups on failure. Date conversion and JSON expression parsing are retained. + +getConceptSetDefinition <- function(conceptSetId, baseUrl, + authHeader = Sys.getenv("ATLAS_TOKEN")) { + for (package in c("httr", "RJSONIO", "lubridate", "rlang")) { + requireInstall(package) + } + checkmate::assertInt(conceptSetId, lower = 1) + checkmate::assertString(baseUrl, min.chars = 1) + checkmate::assertString(authHeader) + baseUrl <- gsub("/+$", "", baseUrl) + if (!grepl("^https?://", baseUrl, ignore.case = TRUE)) { + stop("baseUrl must be an HTTP or HTTPS WebAPI URL.", call. = FALSE) + } + if (nzchar(authHeader) && !grepl("^https://", baseUrl, ignore.case = TRUE)) { + stop("Use HTTPS when sending an ATLAS token.", call. = FALSE) + } + result <- getDefinition(id = conceptSetId, baseUrl = baseUrl, + category = "conceptSet", authHeader = authHeader) + return(result) +} + +getDefinition <- function(id, baseUrl, category, authHeader = "") { + checkmate::assertChoice(category, "conceptSet") + .checkBaseUrl(baseUrl, authHeader) + argument <- .getStandardCategories() + url <- paste0(baseUrl, "/", argument$categoryUrl, "/", id) + response <- httr::content(.GET(url, authHeader = authHeader), + as = "parsed", type = "application/json") + if (!is.list(response) || !is.character(response$name) || + length(response$name) != 1L || is.na(response$name)) { + stop("WebAPI returned an invalid concept-set definition.", call. = FALSE) + } + if (is.null(response$expression)) { + if (!is.null(response$specification)) { + response$expression <- response$specification + response$specification <- NULL + } else if (!is.null(response$design)) { + response$expression <- response$design + response$design <- NULL + } else { + urlExpression <- paste0(url, "/", argument$categoryUrlGetExpression) + response$expression <- httr::content( + .GET(urlExpression, authHeader = authHeader), + as = "parsed", type = "application/json" + ) + } + } + if (is.character(response$expression)) { + if (length(response$expression) != 1L || + !jsonlite::validate(response$expression)) { + stop("WebAPI returned an invalid JSON expression.", call. = FALSE) + } + response$expression <- RJSONIO::fromJSON( + response$expression, nullValue = NA, digits = 23 + ) + for (name in names(response)) { + if (grepl("date", tolower(name))) { + response[[name]] <- .convertToDateTime(response[[name]]) + } + } + } + if (!is.list(response$expression)) { + stop("WebAPI returned an invalid concept-set expression.", call. = FALSE) + } + return(response) +} + +.getStandardCategories <- function() { + data.frame(categoryStandard = "conceptSet", categoryUrl = "conceptset", + categoryUrlGetExpression = "expression") +} + +.checkBaseUrl <- function(baseUrl, authHeader = "") { + webApiVersion <- getWebApiVersion(baseUrl, authHeader) + if (is.null(webApiVersion) || length(webApiVersion) == 0L) { + stop("Could not verify the WebAPI version. Check baseUrl and network access.", + call. = FALSE) + } +} + +getWebApiVersion <- function(baseUrl, authHeader = "") { + response <- .GET(paste0(baseUrl, "/info"), authHeader = authHeader) + httr::content(response, as = "parsed", type = "application/json")$version +} + +.GET <- function(url, authHeader = "") { + .request(url, method = "GET", authHeader = authHeader) +} + +.request <- function(url, method, authHeader = "") { + requireInstall("httr") + requireInstall("rlang") + checkmate::assertChoice(method, "GET") + headers <- httr::accept_json() + if (nzchar(authHeader)) { + if (!grepl("^Bearer[[:space:]]", authHeader, ignore.case = TRUE)) { + authHeader <- paste("Bearer", authHeader) + } + headers <- httr::add_headers(Accept = "application/json", + Authorization = authHeader) + } + response <- httr::GET(url, headers, httr::timeout(30), + httr::config(followlocation = 0L)) + status <- httr::status_code(response) + if (status == 401L) { + rlang::abort("HTTP 401: Unauthorized. Check ATLAS_TOKEN or authHeader.", + class = "webapi-unauthorized") + } + if (status == 403L) { + rlang::abort("HTTP 403: You do not have permission to access this WebAPI resource.", + class = "webapi-forbidden") + } + if (status == 404L) { + rlang::abort("HTTP 404: The WebAPI resource was not found.", + class = "webapi-notfound") + } + if (status == 500L) { + rlang::abort("HTTP 500: The WebAPI encountered an internal server error.", + class = "webapi-server") + } + httr::stop_for_status(response) + if (status != 200L) { + stop(sprintf("Unexpected WebAPI HTTP status: %s.", status), call. = FALSE) + } + response +} + +.millisecondsToDate <- function(milliseconds) { + if (is.numeric(milliseconds)) { + milliseconds <- lubridate::as_datetime(milliseconds / 1000, + tz = Sys.timezone()) + } + milliseconds +} + +.convertToDateTime <- function(x) { + requireInstall("lubridate") + if (is.numeric(x)) { + x <- .millisecondsToDate(x) + } else if (is.character(x)) { + x <- stringr::str_trim(x) + x <- lubridate::as_datetime( + x, tz = Sys.timezone(), + format = lubridate::guess_formats( + x, orders = c("y-m-d H:M", "y-m-d H:M:S", "ymdHMS", "ymd HMS") + )[1] + ) + } + x +} diff --git a/README.Rmd b/README.Rmd index 4193451..0fccd18 100644 --- a/README.Rmd +++ b/README.Rmd @@ -26,9 +26,12 @@ study packages developed at the Erasmus MC Department of Medical Informatics. ## Installation -You can install the development version of studyGenerics from [GitHub](https://github.com/) with: +You can install studyGenerics from CRAN or the latest development version from [GitHub](https://github.com/) with: ``` r +# CRAN download +install.packages("studyGenerics") + # install.packages("remotes") remotes::install_github("mi-erasmusmc/studyGenerics") ``` diff --git a/README.md b/README.md index 9c35512..bf7754a 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,13 @@ Medical Informatics. ## Installation -You can install the development version of studyGenerics from -[GitHub](https://github.com/) with: +You can install studyGenerics from CRAN or the latest development +version from [GitHub](https://github.com/) with: ``` r +# CRAN download +install.packages("studyGenerics") + # install.packages("remotes") remotes::install_github("mi-erasmusmc/studyGenerics") ``` diff --git a/cran-comments.md b/cran-comments.md index 5990d62..5c5578a 100644 --- a/cran-comments.md +++ b/cran-comments.md @@ -1,9 +1,11 @@ ## R CMD check results -0 errors | 0 warnings | 1 note +0 errors | 0 warnings | 0 notes -* This is a new release. +* This is a new submission. -* ROhdsiWebApi is an optional Suggests dependency used only by - pullConceptsAtlas(). It is available from - https://github.com/OHDSI/ROhdsiWebApi and is used conditionally. +* The package includes adapted concept-set retrieval helpers from + ROhdsiWebApi, licensed under Apache License 2.0. Upstream copyright, + source revision, and modifications are documented in `inst/COPYRIGHTS`. + studyGenerics does not depend on ROhdsiWebApi; the optional dependencies + used by these helpers (httr, RJSONIO, lubridate, and rlang) are on CRAN. diff --git a/inst/COPYRIGHTS b/inst/COPYRIGHTS new file mode 100644 index 0000000..901e7bc --- /dev/null +++ b/inst/COPYRIGHTS @@ -0,0 +1,34 @@ +The internal WebAPI retrieval helpers below pullConceptsAtlas() in +R/pullConceptsAtlas.R are adapted from OHDSI/ROhdsiWebApi, version 1.3.3, +commit 5911ab0973ee18c5e72b62c6b999dc1f1bb21e79: +https://github.com/OHDSI/ROhdsiWebApi/tree/5911ab0973ee18c5e72b62c6b999dc1f1bb21e79 + +Copyright 2022 Observational Health Data Sciences and Informatics. +Upstream package authors: Gowtham Rao, Martijn Schuemie, Jamie Gilbert. +License: Apache License, Version 2.0. +https://www.apache.org/licenses/LICENSE-2.0 +The full license is supplied in the repository's LICENSE.md and is a +standard license referenced by the package DESCRIPTION on CRAN. + +Upstream files and adapted functions: +- R/AutoGeneratedDefinitions.R: getConceptSetDefinition. + Its header says "This file is part of FeatureExtraction"; this is retained + as an upstream attribution in the source comments. +- R/GetPostDeleteDefinition.R: getDefinition. +- R/Private.R: .checkBaseUrl, .getStandardCategories, + .millisecondsToDate, .convertToDateTime. +- R/WebApi.R: getWebApiVersion. +- R/httrWrappers.R: .GET, .request. + +Modifications for studyGenerics: +- Keep the helpers internal and support only concept-set retrieval. +- Check optional dependencies through studyGenerics:::requireInstall(). +- Pass authentication explicitly per request instead of caching headers in + ROhdsiWebApi's environment. Accept ATLAS_TOKEN with or without Bearer. +- Authenticate version checks, require HTTPS for tokens, disable redirects, + and limit each HTTP request to 30 seconds. +- Validate inputs and responses; handle HTTP failures without fetching the + metadata of unrelated definitions. Retain expression and date conversion. + +No upstream NOTICE file was present at the cited commit. These attributions +do not replace or modify the Apache License 2.0 terms. diff --git a/man/pullConceptsAtlas.Rd b/man/pullConceptsAtlas.Rd index 69964ce..d9ff9cc 100644 --- a/man/pullConceptsAtlas.Rd +++ b/man/pullConceptsAtlas.Rd @@ -9,7 +9,8 @@ pullConceptsAtlas( conceptSetType, baseUrl = "https://atlas.darwin-eu.org/WebAPI", deletePrevious = TRUE, - patternToRemove = c("p[0-9]{1}_c[0-9]{1}_[0-9]{3}_") + patternToRemove = c("p[0-9]{1}_c[0-9]{1}_[0-9]{3}_"), + authHeader = Sys.getenv("ATLAS_TOKEN") ) } \arguments{ @@ -25,16 +26,27 @@ the target \verb{} folder before downloading the new concept set \item{patternToRemove}{A vector of regular expressions containing the pattern(s) to be removed from the ATLAS concept set names before creating the output JSON files.} + +\item{authHeader}{Authorization header for the WebAPI. Defaults to +\code{Sys.getenv("ATLAS_TOKEN")}. Use \code{""} for a public WebAPI.} } \value{ A tibble with one row per downloaded concept set and the columns -\code{conceptSetId}, \code{originalName}, \code{cleanName}, and \code{jsonPath}. +\code{conceptSetId}, \code{originalName}, \code{cleanName}, and \code{fileName}. } \description{ Retrieves each concept set definition from the ATLAS WebAPI, cleans the concept set name for use as a file name and writes the JSON definition to \verb{inst/concept_sets//}. } +\details{ +Requires the optional CRAN packages \code{httr}, \code{RJSONIO}, \code{lubridate}, and \code{rlang}. +For a protected WebAPI, set \code{ATLAS_TOKEN} in your local \code{.Renviron} file and +restart R. The token may include the \code{Bearer } prefix. It is sent only to +the supplied WebAPI. Do not commit the token to version control. +The internal retrieval helpers are adapted from OHDSI's \code{ROhdsiWebApi} +under the Apache License 2.0; see \code{inst/COPYRIGHTS} in the package source. +} \examples{ \dontrun{ diff --git a/tests/testthat/test-pullConceptsAtlas.R b/tests/testthat/test-pullConceptsAtlas.R index 5574499..7212ec7 100644 --- a/tests/testthat/test-pullConceptsAtlas.R +++ b/tests/testthat/test-pullConceptsAtlas.R @@ -1,40 +1,39 @@ -test_that("pullConceptsAtlas downloads concept sets correctly", { - +test_that("pullConceptsAtlas downloads concept sets and saves valid JSON files", { skip_on_cran() - skip_if_not_installed("ROhdsiWebApi") - skip_if(Sys.getenv("ATLAS_TOKEN") == "") - - require(ROhdsiWebApi) - - baseUrl <- "https://atlas.darwin-eu.org/WebAPI" - token <- Sys.getenv("ATLAS_TOKEN") - ROhdsiWebApi::setAuthHeader(baseUrl, authHeader = token) - ROhdsiWebApi::getCdmSources(baseUrl) + skip_if( + !nzchar(Sys.getenv("ATLAS_TOKEN")), + "ATLAS_TOKEN is not set; live test skipped" + ) + for (package in c("httr", "RJSONIO", "lubridate", "rlang")) { + skip_if_not_installed(package) + } + + # Save the real ATLAS downloads in a temporary study folder. + testProject <- withr::local_tempdir() + local_mocked_bindings( + here = function(...) file.path(testProject, ...), + .package = "here" + ) downloaded_concepts <- pullConceptsAtlas( conceptSetList = c(5305, 5293), conceptSetType = "cancer_cohorts", - baseUrl, + baseUrl = "https://atlas.darwin-eu.org/WebAPI", deletePrevious = TRUE, patternToRemove = c("p4-c4-002", "onconet", "hierarchy_based") ) - downloaded_concepts |> - dplyr::filter(conceptSetId %in% c(5305, 5293)) |> - nrow() |> - expect_equal(2) - - downloaded_concepts |> - dplyr::pull(fileName) |> - stringr::str_detect("lung_cancer") |> - expect_all_true() - - unlink( - here::here( - "inst/concept_sets", - "cancer_cohorts" - ), - recursive = TRUE - ) - + # Expect both lung cancer concept sets and their saved definitions. + expect_equal(downloaded_concepts$conceptSetId, c(5305, 5293)) + expect_true(all(grepl("lung_cancer", downloaded_concepts$fileName))) + + for (file_name in downloaded_concepts$fileName) { + output <- file.path( + testProject, "inst", "concept_sets", "cancer_cohorts", file_name + ) + expect_true(file.exists(output)) + definition <- jsonlite::fromJSON(output, simplifyVector = FALSE) + expect_true("items" %in% names(definition)) + expect_gt(length(definition$items), 0) + } })