From 39e156beb66c3d3b493a30f6165f3c1e86e6cbf1 Mon Sep 17 00:00:00 2001 From: cebarboza Date: Thu, 17 Sep 2026 16:24:03 +0200 Subject: [PATCH 01/10] CRAN submission --- .Rbuildignore | 1 + CRAN-SUBMISSION | 3 +++ README.Rmd | 1 + README.md | 2 ++ 4 files changed, 7 insertions(+) create mode 100644 CRAN-SUBMISSION diff --git a/.Rbuildignore b/.Rbuildignore index e494ae9..2436c7b 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -18,3 +18,4 @@ ^doc$ ^Meta$ ^AGENTS\.md$ +^CRAN-SUBMISSION$ diff --git a/CRAN-SUBMISSION b/CRAN-SUBMISSION new file mode 100644 index 0000000..7e53358 --- /dev/null +++ b/CRAN-SUBMISSION @@ -0,0 +1,3 @@ +Version: 0.1.3 +Date: 2026-09-17 14:21:09 UTC +SHA: 7eedb371a1ad3199cb12fde4b483dcb397cf571b diff --git a/README.Rmd b/README.Rmd index 4193451..918f463 100644 --- a/README.Rmd +++ b/README.Rmd @@ -17,6 +17,7 @@ knitr::opts_chunk$set( # studyGenerics +[![CRAN status](https://www.r-pkg.org/badges/version/studyGenerics)](https://CRAN.R-project.org/package=studyGenerics) [![R-CMD-check](https://github.com/mi-erasmusmc/studyGenerics/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/mi-erasmusmc/studyGenerics/actions/workflows/R-CMD-check.yaml) [![Codecov test coverage](https://codecov.io/gh/mi-erasmusmc/studyGenerics/graph/badge.svg)](https://app.codecov.io/gh/mi-erasmusmc/studyGenerics) diff --git a/README.md b/README.md index 9c35512..b8a0897 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ +[![CRAN +status](https://www.r-pkg.org/badges/version/studyGenerics)](https://CRAN.R-project.org/package=studyGenerics) [![R-CMD-check](https://github.com/mi-erasmusmc/studyGenerics/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/mi-erasmusmc/studyGenerics/actions/workflows/R-CMD-check.yaml) [![Codecov test coverage](https://codecov.io/gh/mi-erasmusmc/studyGenerics/graph/badge.svg)](https://app.codecov.io/gh/mi-erasmusmc/studyGenerics) From cbb7bc4eaf9d6a43005ea0eed6b1e1dabb9929f0 Mon Sep 17 00:00:00 2001 From: cebarboza Date: Thu, 17 Sep 2026 16:28:06 +0200 Subject: [PATCH 02/10] update readme CRAN --- README.Rmd | 5 ++++- README.md | 7 +++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/README.Rmd b/README.Rmd index 918f463..7d587db 100644 --- a/README.Rmd +++ b/README.Rmd @@ -27,9 +27,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 b8a0897..563f249 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,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") ``` From 709672f6be81395f716b699c568f10f930093bfa Mon Sep 17 00:00:00 2001 From: cebarboza Date: Thu, 17 Sep 2026 17:07:20 +0200 Subject: [PATCH 03/10] codecov coverage --- .github/workflows/test-coverage.yaml | 60 ++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 .github/workflows/test-coverage.yaml diff --git a/.github/workflows/test-coverage.yaml b/.github/workflows/test-coverage.yaml new file mode 100644 index 0000000..d10726e --- /dev/null +++ b/.github/workflows/test-coverage.yaml @@ -0,0 +1,60 @@ +# 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 }} + + 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 From c047a63c4b4806f56799a92edc19a47fb10d7808 Mon Sep 17 00:00:00 2001 From: cebarboza Date: Thu, 17 Sep 2026 17:09:48 +0200 Subject: [PATCH 04/10] fix coverage --- .github/workflows/test-coverage.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-coverage.yaml b/.github/workflows/test-coverage.yaml index d10726e..65b1112 100644 --- a/.github/workflows/test-coverage.yaml +++ b/.github/workflows/test-coverage.yaml @@ -22,7 +22,10 @@ jobs: - uses: r-lib/actions/setup-r-dependencies@v2 with: - extra-packages: any::covr, any::xml2 + extra-packages: | + any::covr + any::xml2 + github::OHDSI/ROhdsiWebApi needs: coverage - name: Test coverage From 52023c654c2291a195ea0de4da6db4d2c32417ad Mon Sep 17 00:00:00 2001 From: cebarboza Date: Thu, 17 Sep 2026 17:11:35 +0200 Subject: [PATCH 05/10] cran badge removed --- README.Rmd | 1 - README.md | 2 -- 2 files changed, 3 deletions(-) diff --git a/README.Rmd b/README.Rmd index 7d587db..0fccd18 100644 --- a/README.Rmd +++ b/README.Rmd @@ -17,7 +17,6 @@ knitr::opts_chunk$set( # studyGenerics -[![CRAN status](https://www.r-pkg.org/badges/version/studyGenerics)](https://CRAN.R-project.org/package=studyGenerics) [![R-CMD-check](https://github.com/mi-erasmusmc/studyGenerics/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/mi-erasmusmc/studyGenerics/actions/workflows/R-CMD-check.yaml) [![Codecov test coverage](https://codecov.io/gh/mi-erasmusmc/studyGenerics/graph/badge.svg)](https://app.codecov.io/gh/mi-erasmusmc/studyGenerics) diff --git a/README.md b/README.md index 563f249..bf7754a 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,6 @@ -[![CRAN -status](https://www.r-pkg.org/badges/version/studyGenerics)](https://CRAN.R-project.org/package=studyGenerics) [![R-CMD-check](https://github.com/mi-erasmusmc/studyGenerics/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/mi-erasmusmc/studyGenerics/actions/workflows/R-CMD-check.yaml) [![Codecov test coverage](https://codecov.io/gh/mi-erasmusmc/studyGenerics/graph/badge.svg)](https://app.codecov.io/gh/mi-erasmusmc/studyGenerics) From b488cffd9a20bcd30e36084a54a6590e3f8847cb Mon Sep 17 00:00:00 2001 From: cebarboza Date: Thu, 17 Sep 2026 17:14:13 +0200 Subject: [PATCH 06/10] deleting CRAN submission --- CRAN-SUBMISSION | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 CRAN-SUBMISSION diff --git a/CRAN-SUBMISSION b/CRAN-SUBMISSION deleted file mode 100644 index 7e53358..0000000 --- a/CRAN-SUBMISSION +++ /dev/null @@ -1,3 +0,0 @@ -Version: 0.1.3 -Date: 2026-09-17 14:21:09 UTC -SHA: 7eedb371a1ad3199cb12fde4b483dcb397cf571b From 29206c869543828cb5e57f8ee504993cc57c55e9 Mon Sep 17 00:00:00 2001 From: cebarboza Date: Thu, 17 Sep 2026 17:46:24 +0200 Subject: [PATCH 07/10] fixes --- .Rbuildignore | 1 + .github/workflows/R-CMD-check.yaml | 1 - .github/workflows/pkgdown.yaml | 1 - .github/workflows/test-coverage.yaml | 1 - .gitignore | 2 + DESCRIPTION | 23 ++- R/pullConceptsAtlas.R | 214 ++++++++++++++++++- cran-comments.md | 8 +- inst/COPYRIGHTS | 34 +++ man/pullConceptsAtlas.Rd | 16 +- tests/testthat/test-pullConceptsAtlas.R | 262 +++++++++++++++++++++--- 11 files changed, 511 insertions(+), 52 deletions(-) create mode 100644 inst/COPYRIGHTS diff --git a/.Rbuildignore b/.Rbuildignore index 2436c7b..c05e3bc 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -1,4 +1,5 @@ ^renv$ +^\.[Rr]environ$ ^renv\.lock$ ^.*\.Rproj$ ^\.Rproj\.user$ 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 index 65b1112..8297e09 100644 --- a/.github/workflows/test-coverage.yaml +++ b/.github/workflows/test-coverage.yaml @@ -25,7 +25,6 @@ jobs: extra-packages: | any::covr any::xml2 - github::OHDSI/ROhdsiWebApi needs: coverage - name: Test coverage 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..4d90708 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -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/R/pullConceptsAtlas.R b/R/pullConceptsAtlas.R index 1bd86f9..ac64d58 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")) { + studyGenerics:::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")) { + studyGenerics:::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 = "") { + studyGenerics:::requireInstall("httr") + studyGenerics:::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) { + studyGenerics:::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/cran-comments.md b/cran-comments.md index 5990d62..117043a 100644 --- a/cran-comments.md +++ b/cran-comments.md @@ -4,6 +4,8 @@ * This is a new release. -* ROhdsiWebApi is an optional Suggests dependency used only by - pullConceptsAtlas(). It is available from - https://github.com/OHDSI/ROhdsiWebApi and is used conditionally. +* Concept-set retrieval helpers from ROhdsiWebApi are bundled under + Apache-2.0, with upstream copyright notices, source revision, and changes + documented in R/pullConceptsAtlas.R and inst/COPYRIGHTS. The package no + longer depends on ROhdsiWebApi. The replacement optional dependencies + (httr, RJSONIO, lubridate, and rlang) are available 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..d3484b5 100644 --- a/tests/testthat/test-pullConceptsAtlas.R +++ b/tests/testthat/test-pullConceptsAtlas.R @@ -1,40 +1,244 @@ -test_that("pullConceptsAtlas downloads concept sets correctly", { +# Create a WebAPI response without contacting the server. +webApiResponse <- function(body, status = 200L) { + structure( + list( + status_code = status, + headers = list(`content-type` = "application/json"), + content = charToRaw(body), + url = "https://example.org/WebAPI" + ), + class = "response" + ) +} - skip_on_cran() - skip_if_not_installed("ROhdsiWebApi") - skip_if(Sys.getenv("ATLAS_TOKEN") == "") +# These packages are optional for studyGenerics, but needed for these tests. +skipWithoutWebApiDependencies <- function() { + for (package in c("httr", "RJSONIO", "lubridate", "rlang")) { + skip_if_not_installed(package) + } +} - require(ROhdsiWebApi) +test_that("getConceptSetDefinition downloads the definition and its concept list", { + skipWithoutWebApiDependencies() - baseUrl <- "https://atlas.darwin-eu.org/WebAPI" - token <- Sys.getenv("ATLAS_TOKEN") - ROhdsiWebApi::setAuthHeader(baseUrl, authHeader = token) - ROhdsiWebApi::getCdmSources(baseUrl) + # Record the requests and return example responses. + urls <- character() + local_mocked_bindings(GET = function(url, ...) { + urls <<- c(urls, url) + configs <- list(...) + headers <- unlist(lapply(configs, function(x) x$headers)) + expect_equal(unname(headers["Authorization"]), "Bearer example-token") + request_options <- unlist(lapply(configs, function(x) x$options)) + expect_equal(unname(request_options["followlocation"]), 0) + if (endsWith(url, "/info")) { + return(webApiResponse('{"version":"2.14.0"}')) + } + if (endsWith(url, "/expression")) { + return(webApiResponse('{"items":[{"concept":{"CONCEPT_ID":123},"isExcluded":false}]}')) + } + webApiResponse('{"id":1,"name":"Example"}') + }, .package = "httr") - downloaded_concepts <- pullConceptsAtlas( - conceptSetList = c(5305, 5293), - conceptSetType = "cancer_cohorts", - baseUrl, - deletePrevious = TRUE, - patternToRemove = c("p4-c4-002", "onconet", "hierarchy_based") + # Get the concept set using a test token. + result <- getConceptSetDefinition( + conceptSetId = 1, + baseUrl = "https://example.org/WebAPI/", + authHeader = "example-token" + ) + + # Expect a version check, a definition request, and a concept-list request. + expect_equal( + urls, + paste0( + "https://example.org/WebAPI", + c("/info", "/conceptset/1", "/conceptset/1/expression") + ) ) + expect_equal(result$name, "Example") + expect_equal(result$expression$items[[1]]$concept$CONCEPT_ID, 123) + expect_false(result$expression$items[[1]]$isExcluded) +}) - downloaded_concepts |> - dplyr::filter(conceptSetId %in% c(5305, 5293)) |> - nrow() |> - expect_equal(2) +test_that("getConceptSetDefinition reads the different WebAPI expression formats", { + skipWithoutWebApiDependencies() - downloaded_concepts |> - dplyr::pull(fileName) |> - stringr::str_detect("lung_cancer") |> - expect_all_true() + # Return the definition directly, without a separate concept-list request. + definition <- list(name = "Example", expression = list(items = list())) + local_mocked_bindings(.GET = function(url, authHeader = "") { + if (endsWith(url, "/info")) { + return(webApiResponse('{"version":"2.14.0"}')) + } + expect_false(endsWith(url, "/expression")) + webApiResponse(jsonlite::toJSON(definition, auto_unbox = TRUE)) + }) - unlink( - here::here( - "inst/concept_sets", - "cancer_cohorts" - ), - recursive = TRUE + # WebAPI may store the concept list under any of these names. + for (field in c("expression", "specification", "design")) { + definition <- list(name = "Example") + definition[[field]] <- list(items = list()) + result <- getConceptSetDefinition( + conceptSetId = 1, + baseUrl = "https://example.org/WebAPI", + authHeader = "" + ) + expect_equal(result$expression, list(items = list())) + } + + # A JSON string is parsed, and a date in milliseconds is converted. + definition <- list(name = "Example", expression = '{"items":[]}', createdDate = 0) + result <- getConceptSetDefinition( + conceptSetId = 1, + baseUrl = "https://example.org/WebAPI", + authHeader = "" + ) + expect_true(is.list(result$expression)) + expect_s3_class(result$createdDate, "POSIXct") + expect_equal(as.numeric(result$createdDate), 0) + # Invalid JSON should give a clear error. + definition$expression <- "invalid json" + expect_error( + getConceptSetDefinition(1, "https://example.org/WebAPI", authHeader = ""), + "invalid JSON expression" + ) +}) + +test_that("WebAPI errors do not include the token", { + skipWithoutWebApiDependencies() + + # Return each HTTP error without making a real request. + status <- 401L + local_mocked_bindings(GET = function(...) webApiResponse("{}", status), + .package = "httr") + expected_errors <- c( + "401" = "webapi-unauthorized", + "403" = "webapi-forbidden", + "404" = "webapi-notfound", + "500" = "webapi-server" ) + for (status_code in names(expected_errors)) { + status <- as.integer(status_code) + error <- tryCatch( + .GET("https://example.org/WebAPI/info", authHeader = "example-token"), + error = identity + ) + expect_s3_class(error, expected_errors[[status_code]]) + expect_false(grepl("example-token", conditionMessage(error), fixed = TRUE)) + } + # Redirects should also stop the request. + status <- 302L + expect_error(.GET("https://example.org/WebAPI/info"), class = "http_302") +}) + +test_that("getConceptSetDefinition checks the URL and concept-set ID", { + skipWithoutWebApiDependencies() + + # Tokens require HTTPS, and concept-set IDs must be whole numbers. + expect_error( + getConceptSetDefinition(1, "http://example.org/WebAPI", authHeader = "example-token"), + "Use HTTPS" + ) + expect_error( + getConceptSetDefinition(1.5, "https://example.org/WebAPI", authHeader = "") + ) +}) +test_that("public WebAPI requests work without a token and reject incomplete responses", { + skipWithoutWebApiDependencies() + + # Check that public requests have no authorization header. + local_mocked_bindings(GET = function(url, ...) { + headers <- unlist(lapply(list(...), function(x) x$headers)) + expect_false("Authorization" %in% names(headers)) + webApiResponse("{}") + }, .package = "httr") + # The server must provide its version and a valid concept-set definition. + expect_error( + getConceptSetDefinition(1, "https://example.org/WebAPI", authHeader = ""), + "Could not verify the WebAPI version" + ) + local_mocked_bindings(getWebApiVersion = function(...) "2.14.0") + expect_error( + getConceptSetDefinition(1, "https://example.org/WebAPI", authHeader = ""), + "invalid concept-set definition" + ) +}) + +test_that("pullConceptsAtlas saves concept sets with clean file names", { + skipWithoutWebApiDependencies() + + # Save files in a temporary study folder. + test_root <- withr::local_tempdir() + local_mocked_bindings(here = function(...) file.path(test_root, ...), .package = "here") + # Return two example concept sets instead of contacting ATLAS. + local_mocked_bindings(.GET = function(url, authHeader = "") { + if (endsWith(url, "/info")) { + return(webApiResponse('{"version":"2.14.0"}')) + } + id <- as.numeric(basename(url)) + webApiResponse(jsonlite::toJSON(list( + id = id, name = paste(id, "Lung Cancer"), + expression = list(items = list(list(concept = list(CONCEPT_ID = id)))) + ), auto_unbox = TRUE)) + }) + result <- pullConceptsAtlas( + conceptSetList = c(1, 2), + conceptSetType = "example", + baseUrl = "https://example.org/WebAPI", + authHeader = "" + ) + + # Check the returned summary and the contents of each saved file. + expect_equal(result$conceptSetId, c(1, 2)) + expect_equal(result$fileName, c("1_lung_cancer.json", "2_lung_cancer.json")) + for (i in seq_len(nrow(result))) { + output <- file.path(test_root, "inst", "concept_sets", "example", result$fileName[i]) + expect_true(file.exists(output)) + expression <- jsonlite::fromJSON(output, simplifyVector = FALSE) + expect_equal(expression$items[[1]]$concept$CONCEPT_ID, i) + } +}) + +test_that("pullConceptsAtlas stops before creating files if httr is missing", { + test_root <- withr::local_tempdir() + local_mocked_bindings(here = function(...) file.path(test_root, ...), .package = "here") + # Simulate a missing package without uninstalling anything. + local_mocked_bindings(requireInstall = function(package) { + if (package == "httr") { + stop("'httr' must be installed to use this function.") + } + }) + expect_error( + pullConceptsAtlas(conceptSetList = 1, conceptSetType = "example", authHeader = ""), + "'httr' must be installed" + ) + expect_false(dir.exists(file.path(test_root, "inst"))) +}) + +test_that("pullConceptsAtlas downloads real ATLAS concept sets using ATLAS_TOKEN", { + skip_on_cran() + skipWithoutWebApiDependencies() + skip_if( + !nzchar(Sys.getenv("ATLAS_TOKEN")), + "ATLAS_TOKEN is not set; live test skipped" + ) + + # Use the real server, but save the downloads in a temporary folder. + test_root <- withr::local_tempdir() + local_mocked_bindings(here = function(...) file.path(test_root, ...), .package = "here") + + downloaded_concepts <- pullConceptsAtlas( + conceptSetList = c(5305, 5293), + conceptSetType = "cancer_cohorts", + baseUrl = "https://atlas.darwin-eu.org/WebAPI", + deletePrevious = TRUE, + patternToRemove = c("p4-c4-002", "onconet", "hierarchy_based") + ) + # Expect both lung cancer concept sets and valid JSON files. + 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(test_root, "inst", "concept_sets", "cancer_cohorts", file_name) + expect_true(file.exists(output)) + expect_true(jsonlite::validate(paste(readLines(output), collapse = "\n"))) + } }) From f9984db80385e040e94375e782aada89fa3e5a2d Mon Sep 17 00:00:00 2001 From: cebarboza Date: Thu, 17 Sep 2026 17:46:37 +0200 Subject: [PATCH 08/10] more simple test --- tests/testthat/test-pullConceptsAtlas.R | 243 ++---------------------- 1 file changed, 19 insertions(+), 224 deletions(-) diff --git a/tests/testthat/test-pullConceptsAtlas.R b/tests/testthat/test-pullConceptsAtlas.R index d3484b5..7212ec7 100644 --- a/tests/testthat/test-pullConceptsAtlas.R +++ b/tests/testthat/test-pullConceptsAtlas.R @@ -1,230 +1,19 @@ -# Create a WebAPI response without contacting the server. -webApiResponse <- function(body, status = 200L) { - structure( - list( - status_code = status, - headers = list(`content-type` = "application/json"), - content = charToRaw(body), - url = "https://example.org/WebAPI" - ), - class = "response" - ) -} - -# These packages are optional for studyGenerics, but needed for these tests. -skipWithoutWebApiDependencies <- function() { - for (package in c("httr", "RJSONIO", "lubridate", "rlang")) { - skip_if_not_installed(package) - } -} - -test_that("getConceptSetDefinition downloads the definition and its concept list", { - skipWithoutWebApiDependencies() - - # Record the requests and return example responses. - urls <- character() - local_mocked_bindings(GET = function(url, ...) { - urls <<- c(urls, url) - configs <- list(...) - headers <- unlist(lapply(configs, function(x) x$headers)) - expect_equal(unname(headers["Authorization"]), "Bearer example-token") - request_options <- unlist(lapply(configs, function(x) x$options)) - expect_equal(unname(request_options["followlocation"]), 0) - if (endsWith(url, "/info")) { - return(webApiResponse('{"version":"2.14.0"}')) - } - if (endsWith(url, "/expression")) { - return(webApiResponse('{"items":[{"concept":{"CONCEPT_ID":123},"isExcluded":false}]}')) - } - webApiResponse('{"id":1,"name":"Example"}') - }, .package = "httr") - - # Get the concept set using a test token. - result <- getConceptSetDefinition( - conceptSetId = 1, - baseUrl = "https://example.org/WebAPI/", - authHeader = "example-token" - ) - - # Expect a version check, a definition request, and a concept-list request. - expect_equal( - urls, - paste0( - "https://example.org/WebAPI", - c("/info", "/conceptset/1", "/conceptset/1/expression") - ) - ) - expect_equal(result$name, "Example") - expect_equal(result$expression$items[[1]]$concept$CONCEPT_ID, 123) - expect_false(result$expression$items[[1]]$isExcluded) -}) - -test_that("getConceptSetDefinition reads the different WebAPI expression formats", { - skipWithoutWebApiDependencies() - - # Return the definition directly, without a separate concept-list request. - definition <- list(name = "Example", expression = list(items = list())) - local_mocked_bindings(.GET = function(url, authHeader = "") { - if (endsWith(url, "/info")) { - return(webApiResponse('{"version":"2.14.0"}')) - } - expect_false(endsWith(url, "/expression")) - webApiResponse(jsonlite::toJSON(definition, auto_unbox = TRUE)) - }) - - # WebAPI may store the concept list under any of these names. - for (field in c("expression", "specification", "design")) { - definition <- list(name = "Example") - definition[[field]] <- list(items = list()) - result <- getConceptSetDefinition( - conceptSetId = 1, - baseUrl = "https://example.org/WebAPI", - authHeader = "" - ) - expect_equal(result$expression, list(items = list())) - } - - # A JSON string is parsed, and a date in milliseconds is converted. - definition <- list(name = "Example", expression = '{"items":[]}', createdDate = 0) - result <- getConceptSetDefinition( - conceptSetId = 1, - baseUrl = "https://example.org/WebAPI", - authHeader = "" - ) - expect_true(is.list(result$expression)) - expect_s3_class(result$createdDate, "POSIXct") - expect_equal(as.numeric(result$createdDate), 0) - # Invalid JSON should give a clear error. - definition$expression <- "invalid json" - expect_error( - getConceptSetDefinition(1, "https://example.org/WebAPI", authHeader = ""), - "invalid JSON expression" - ) -}) - -test_that("WebAPI errors do not include the token", { - skipWithoutWebApiDependencies() - - # Return each HTTP error without making a real request. - status <- 401L - local_mocked_bindings(GET = function(...) webApiResponse("{}", status), - .package = "httr") - expected_errors <- c( - "401" = "webapi-unauthorized", - "403" = "webapi-forbidden", - "404" = "webapi-notfound", - "500" = "webapi-server" - ) - for (status_code in names(expected_errors)) { - status <- as.integer(status_code) - error <- tryCatch( - .GET("https://example.org/WebAPI/info", authHeader = "example-token"), - error = identity - ) - expect_s3_class(error, expected_errors[[status_code]]) - expect_false(grepl("example-token", conditionMessage(error), fixed = TRUE)) - } - # Redirects should also stop the request. - status <- 302L - expect_error(.GET("https://example.org/WebAPI/info"), class = "http_302") -}) - -test_that("getConceptSetDefinition checks the URL and concept-set ID", { - skipWithoutWebApiDependencies() - - # Tokens require HTTPS, and concept-set IDs must be whole numbers. - expect_error( - getConceptSetDefinition(1, "http://example.org/WebAPI", authHeader = "example-token"), - "Use HTTPS" - ) - expect_error( - getConceptSetDefinition(1.5, "https://example.org/WebAPI", authHeader = "") - ) -}) - -test_that("public WebAPI requests work without a token and reject incomplete responses", { - skipWithoutWebApiDependencies() - - # Check that public requests have no authorization header. - local_mocked_bindings(GET = function(url, ...) { - headers <- unlist(lapply(list(...), function(x) x$headers)) - expect_false("Authorization" %in% names(headers)) - webApiResponse("{}") - }, .package = "httr") - # The server must provide its version and a valid concept-set definition. - expect_error( - getConceptSetDefinition(1, "https://example.org/WebAPI", authHeader = ""), - "Could not verify the WebAPI version" - ) - local_mocked_bindings(getWebApiVersion = function(...) "2.14.0") - expect_error( - getConceptSetDefinition(1, "https://example.org/WebAPI", authHeader = ""), - "invalid concept-set definition" - ) -}) - -test_that("pullConceptsAtlas saves concept sets with clean file names", { - skipWithoutWebApiDependencies() - - # Save files in a temporary study folder. - test_root <- withr::local_tempdir() - local_mocked_bindings(here = function(...) file.path(test_root, ...), .package = "here") - # Return two example concept sets instead of contacting ATLAS. - local_mocked_bindings(.GET = function(url, authHeader = "") { - if (endsWith(url, "/info")) { - return(webApiResponse('{"version":"2.14.0"}')) - } - id <- as.numeric(basename(url)) - webApiResponse(jsonlite::toJSON(list( - id = id, name = paste(id, "Lung Cancer"), - expression = list(items = list(list(concept = list(CONCEPT_ID = id)))) - ), auto_unbox = TRUE)) - }) - result <- pullConceptsAtlas( - conceptSetList = c(1, 2), - conceptSetType = "example", - baseUrl = "https://example.org/WebAPI", - authHeader = "" - ) - - # Check the returned summary and the contents of each saved file. - expect_equal(result$conceptSetId, c(1, 2)) - expect_equal(result$fileName, c("1_lung_cancer.json", "2_lung_cancer.json")) - for (i in seq_len(nrow(result))) { - output <- file.path(test_root, "inst", "concept_sets", "example", result$fileName[i]) - expect_true(file.exists(output)) - expression <- jsonlite::fromJSON(output, simplifyVector = FALSE) - expect_equal(expression$items[[1]]$concept$CONCEPT_ID, i) - } -}) - -test_that("pullConceptsAtlas stops before creating files if httr is missing", { - test_root <- withr::local_tempdir() - local_mocked_bindings(here = function(...) file.path(test_root, ...), .package = "here") - # Simulate a missing package without uninstalling anything. - local_mocked_bindings(requireInstall = function(package) { - if (package == "httr") { - stop("'httr' must be installed to use this function.") - } - }) - expect_error( - pullConceptsAtlas(conceptSetList = 1, conceptSetType = "example", authHeader = ""), - "'httr' must be installed" - ) - expect_false(dir.exists(file.path(test_root, "inst"))) -}) - -test_that("pullConceptsAtlas downloads real ATLAS concept sets using ATLAS_TOKEN", { +test_that("pullConceptsAtlas downloads concept sets and saves valid JSON files", { skip_on_cran() - skipWithoutWebApiDependencies() 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) + } - # Use the real server, but save the downloads in a temporary folder. - test_root <- withr::local_tempdir() - local_mocked_bindings(here = function(...) file.path(test_root, ...), .package = "here") + # 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), @@ -233,12 +22,18 @@ test_that("pullConceptsAtlas downloads real ATLAS concept sets using ATLAS_TOKEN deletePrevious = TRUE, patternToRemove = c("p4-c4-002", "onconet", "hierarchy_based") ) - # Expect both lung cancer concept sets and valid JSON files. + + # 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(test_root, "inst", "concept_sets", "cancer_cohorts", file_name) + output <- file.path( + testProject, "inst", "concept_sets", "cancer_cohorts", file_name + ) expect_true(file.exists(output)) - expect_true(jsonlite::validate(paste(readLines(output), collapse = "\n"))) + definition <- jsonlite::fromJSON(output, simplifyVector = FALSE) + expect_true("items" %in% names(definition)) + expect_gt(length(definition$items), 0) } }) From e3ddc104833d04c855b3372e74cb283053e45c9c Mon Sep 17 00:00:00 2001 From: cebarboza Date: Thu, 17 Sep 2026 17:50:01 +0200 Subject: [PATCH 09/10] Increment version number to 0.1.4 --- DESCRIPTION | 2 +- NEWS.md | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 4d90708..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", 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. From 5fc9e9c126c5b8f3aa4ced65aefe8b6e2d0830c7 Mon Sep 17 00:00:00 2001 From: cebarboza Date: Thu, 17 Sep 2026 17:55:05 +0200 Subject: [PATCH 10/10] cran fixes --- .github/workflows/test-coverage.yaml | 2 ++ NAMESPACE | 2 +- R/pullConceptsAtlas.R | 10 +++++----- cran-comments.md | 14 +++++++------- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/.github/workflows/test-coverage.yaml b/.github/workflows/test-coverage.yaml index 8297e09..0a6579f 100644 --- a/.github/workflows/test-coverage.yaml +++ b/.github/workflows/test-coverage.yaml @@ -14,6 +14,8 @@ jobs: 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 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/R/pullConceptsAtlas.R b/R/pullConceptsAtlas.R index ac64d58..053b1d9 100644 --- a/R/pullConceptsAtlas.R +++ b/R/pullConceptsAtlas.R @@ -65,7 +65,7 @@ pullConceptsAtlas <- function( checkmate::assertLogical(deletePrevious) checkmate::assertCharacter(patternToRemove) for (package in c("httr", "RJSONIO", "lubridate", "rlang")) { - studyGenerics:::requireInstall(package) + requireInstall(package) } checkmate::assertString(authHeader) @@ -215,7 +215,7 @@ pullConceptsAtlas <- function( getConceptSetDefinition <- function(conceptSetId, baseUrl, authHeader = Sys.getenv("ATLAS_TOKEN")) { for (package in c("httr", "RJSONIO", "lubridate", "rlang")) { - studyGenerics:::requireInstall(package) + requireInstall(package) } checkmate::assertInt(conceptSetId, lower = 1) checkmate::assertString(baseUrl, min.chars = 1) @@ -301,8 +301,8 @@ getWebApiVersion <- function(baseUrl, authHeader = "") { } .request <- function(url, method, authHeader = "") { - studyGenerics:::requireInstall("httr") - studyGenerics:::requireInstall("rlang") + requireInstall("httr") + requireInstall("rlang") checkmate::assertChoice(method, "GET") headers <- httr::accept_json() if (nzchar(authHeader)) { @@ -347,7 +347,7 @@ getWebApiVersion <- function(baseUrl, authHeader = "") { } .convertToDateTime <- function(x) { - studyGenerics:::requireInstall("lubridate") + requireInstall("lubridate") if (is.numeric(x)) { x <- .millisecondsToDate(x) } else if (is.character(x)) { diff --git a/cran-comments.md b/cran-comments.md index 117043a..5c5578a 100644 --- a/cran-comments.md +++ b/cran-comments.md @@ -1,11 +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. -* Concept-set retrieval helpers from ROhdsiWebApi are bundled under - Apache-2.0, with upstream copyright notices, source revision, and changes - documented in R/pullConceptsAtlas.R and inst/COPYRIGHTS. The package no - longer depends on ROhdsiWebApi. The replacement optional dependencies - (httr, RJSONIO, lubridate, and rlang) are available on CRAN. +* 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.