Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package: omophub
Title: R Client for the 'OMOPHub' Medical Vocabulary API
Version: 1.6.0
Version: 1.7.0
Authors@R: c(
person("Alex", "Chen", email = "alex@omophub.com", role = c("aut", "cre", "cph")),
person("Observational Health Data Science and Informatics", role = c("cph"))
Expand Down
4 changes: 4 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ S3method(print,omophub_mappings)
S3method(print,omophub_relationships)
S3method(print,omophub_vocabularies)
export(OMOPHubClient)
export(fhir_resolve)
export(fhir_resolve_batch)
export(fhir_resolve_codeable_concept)
export(get_api_key)
export(has_api_key)
export(omophub_fhir_url)
export(set_api_key)
importFrom(R6,R6Class)
importFrom(cli,cli_abort)
Expand Down
31 changes: 31 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,34 @@
# omophub 1.7.0

## New Features

* **Tibble output for batch FHIR resolution**
`FhirResource$resolve_batch()` (and the standalone `fhir_resolve_batch()`)
gained an `as_tibble` parameter. When `as_tibble = TRUE`, the call returns
a flat `tibble::tibble` with one row per input coding and columns for the
source concept, standard concept, target CDM table, mapping type, and
resolution status - ready to pipe into `dplyr` / `tidyr`. The batch
summary (`total` / `resolved` / `failed`) is attached as
`attr(result, "summary")`. Default behavior is unchanged: `as_tibble = FALSE`
still returns the legacy list shape.

* **Standalone wrapper functions**
Thin, pipe-friendly wrappers around the R6 `FhirResource` methods:

- `fhir_resolve(client, ...)`
- `fhir_resolve_batch(client, ...)`
- `fhir_resolve_codeable_concept(client, ...)`

Both the R6 form (`client$fhir$resolve(...)`) and the standalone form
work; pick whichever reads better in your pipeline.

* **FHIR client interop helper**
New exported function `omophub_fhir_url(version)` returning the OMOPHub
FHIR Terminology Service base URL for a given FHIR version (`"r4"`,
`"r4b"`, `"r5"`, `"r6"`). Use it with `httr2`, `fhircrackr`, or any
external FHIR client that wants to talk to OMOPHub's FHIR endpoint
directly.

# omophub 1.6.0

## New Features
Expand Down
188 changes: 184 additions & 4 deletions R/fhir.R
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,23 @@ FhirResource <- R6::R6Class(
#' @param include_recommendations Logical. Default `FALSE`.
#' @param recommendations_limit Integer. Default `5L`.
#' @param include_quality Logical. Default `FALSE`.
#' @param as_tibble Logical. When `TRUE`, returns a [tibble::tibble]
#' with one row per input coding and flat columns for the source
#' concept, standard concept, target CDM table, mapping type, and
#' resolution status. The batch `summary` (total/resolved/failed)
#' is attached as `attr(result, "summary")`. Default `FALSE`
#' keeps the legacy list-shaped return.
#'
#' @returns A list with `results` (per-item) and `summary`
#' (total/resolved/failed).
#' @returns When `as_tibble = FALSE` (default), a list with `results`
#' (per-item) and `summary` (total/resolved/failed). When
#' `as_tibble = TRUE`, a [tibble::tibble] suitable for
#' `dplyr`/`tidyr` pipelines.
resolve_batch = function(codings,
resource_type = NULL,
include_recommendations = FALSE,
recommendations_limit = 5L,
include_quality = FALSE) {
include_quality = FALSE,
as_tibble = FALSE) {
stopifnot(is.list(codings), length(codings) >= 1, length(codings) <= 100)
if (!all(vapply(codings, is.list, logical(1)))) {
cli::cli_abort(c(
Expand All @@ -108,7 +117,12 @@ FhirResource <- R6::R6Class(
}
if (isTRUE(include_quality)) body$include_quality <- TRUE

perform_post(private$.base_req, "fhir/resolve/batch", body = body)
result <- perform_post(private$.base_req, "fhir/resolve/batch", body = body)

if (isTRUE(as_tibble)) {
return(fhir_batch_to_tibble(result, codings))
}
result
},

#' @description
Expand Down Expand Up @@ -168,3 +182,169 @@ compact_list <- function(...) {
args <- list(...)
args[!vapply(args, is.null, logical(1))]
}


#' Flatten a batch resolver response into a tibble.
#'
#' Internal helper used by `FhirResource$resolve_batch(as_tibble = TRUE)`.
#' Produces one row per input coding with flat columns for the source
#' concept, standard concept, target CDM table, and status. The original
#' `summary` list is attached to the returned tibble via
#' `attr(x, "summary")`.
#'
#' @param result The raw list result from `perform_post`.
#' @param codings The original input `codings` list, used to align rows
#' when items fail resolution and no `source_concept` is returned.
#' @returns A [tibble::tibble].
#' @keywords internal
fhir_batch_to_tibble <- function(result, codings) {
items <- result$results %||% list()
n <- length(codings)

make_row <- function(i) {
input_coding <- codings[[i]]
item <- if (i <= length(items)) items[[i]] else NULL

# Failed items: the API returns them without a resolution block.
resolution <- item$resolution
if (is.null(resolution)) {
return(tibble::tibble(
source_system = input_coding$system %||% NA_character_,
source_code = input_coding$code %||% NA_character_,
source_concept_id = NA_integer_,
source_concept_name = NA_character_,
standard_concept_id = NA_integer_,
standard_concept_name = NA_character_,
standard_vocabulary_id = NA_character_,
domain_id = NA_character_,
target_table = NA_character_,
mapping_type = NA_character_,
similarity_score = NA_real_,
status = "failed",
status_detail = item$error %||% "unresolved"
))
}

src <- resolution$source_concept %||% list()
std <- resolution$standard_concept %||% list()

tibble::tibble(
source_system = input_coding$system %||% NA_character_,
source_code = input_coding$code %||% NA_character_,
source_concept_id = src$concept_id %||% NA_integer_,
source_concept_name = src$concept_name %||% NA_character_,
standard_concept_id = std$concept_id %||% NA_integer_,
standard_concept_name = std$concept_name %||% NA_character_,
standard_vocabulary_id = std$vocabulary_id %||% NA_character_,
domain_id = std$domain_id %||% NA_character_,
target_table = resolution$target_table %||% NA_character_,
mapping_type = resolution$mapping_type %||% NA_character_,
similarity_score = resolution$similarity_score %||% NA_real_,
status = "resolved",
status_detail = NA_character_
)
}

rows <- lapply(seq_len(n), make_row)
tbl <- do.call(rbind, rows)
attr(tbl, "summary") <- result$summary
tbl
}


#' Null-coalescing operator.
#' @noRd
`%||%` <- function(a, b) if (is.null(a)) b else a


# =============================================================================
# Standalone wrapper functions
#
# Thin wrappers around the R6 `FhirResource` methods so users can write
# pipe-friendly code without dereferencing the client every call:
#
# client |> fhir_resolve(system = "http://snomed.info/sct", code = "44054006")
#
# These forward all arguments to the underlying R6 method.
# =============================================================================

#' Resolve a FHIR Coding to an OMOP standard concept
#'
#' Standalone wrapper for `client$fhir$resolve()`. See
#' [FhirResource] for full parameter documentation.
#'
#' @param client An [OMOPHubClient] instance.
#' @param ... Arguments passed to `FhirResource$resolve()`.
#' @returns A list with `input` and `resolution`.
#' @seealso [FhirResource], [fhir_resolve_batch()],
#' [fhir_resolve_codeable_concept()]
#' @examples
#' \dontrun{
#' client <- OMOPHubClient$new(api_key = Sys.getenv("OMOPHUB_API_KEY"))
#' fhir_resolve(
#' client,
#' system = "http://snomed.info/sct",
#' code = "44054006",
#' resource_type = "Condition"
#' )
#' }
#' @export
fhir_resolve <- function(client, ...) {
client$fhir$resolve(...)
}


#' Batch-resolve FHIR Codings
#'
#' Standalone wrapper for `client$fhir$resolve_batch()`. When
#' `as_tibble = TRUE`, the result is a flat `tibble` suitable for
#' `dplyr`/`tidyr` pipelines.
#'
#' @param client An [OMOPHubClient] instance.
#' @param ... Arguments passed to `FhirResource$resolve_batch()`.
#' @returns See `FhirResource$resolve_batch()`.
#' @seealso [FhirResource], [fhir_resolve()],
#' [fhir_resolve_codeable_concept()]
#' @examples
#' \dontrun{
#' client <- OMOPHubClient$new(api_key = Sys.getenv("OMOPHUB_API_KEY"))
#' tbl <- fhir_resolve_batch(
#' client,
#' codings = list(
#' list(system = "http://snomed.info/sct", code = "44054006"),
#' list(system = "http://loinc.org", code = "2339-0")
#' ),
#' as_tibble = TRUE
#' )
#' dplyr::filter(tbl, status == "resolved")
#' }
#' @export
fhir_resolve_batch <- function(client, ...) {
client$fhir$resolve_batch(...)
}


#' Resolve a FHIR CodeableConcept with vocabulary preference
#'
#' Standalone wrapper for `client$fhir$resolve_codeable_concept()`.
#'
#' @param client An [OMOPHubClient] instance.
#' @param ... Arguments passed to `FhirResource$resolve_codeable_concept()`.
#' @returns See `FhirResource$resolve_codeable_concept()`.
#' @seealso [FhirResource], [fhir_resolve()], [fhir_resolve_batch()]
#' @examples
#' \dontrun{
#' client <- OMOPHubClient$new(api_key = Sys.getenv("OMOPHUB_API_KEY"))
#' fhir_resolve_codeable_concept(
#' client,
#' coding = list(
#' list(system = "http://snomed.info/sct", code = "44054006"),
#' list(system = "http://hl7.org/fhir/sid/icd-10-cm", code = "E11.9")
#' ),
#' resource_type = "Condition"
#' )
#' }
#' @export
fhir_resolve_codeable_concept <- function(client, ...) {
client$fhir$resolve_codeable_concept(...)
}
41 changes: 41 additions & 0 deletions R/fhir_interop.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# FHIR client interop helpers for the OMOPHub FHIR Terminology Service.
#
# These helpers make it easy to configure an external FHIR client (like
# `httr2`, `fhircrackr`, or a custom Spring Security OAuth2 client) to
# talk directly to OMOPHub's FHIR endpoint. They are intentionally thin
# and have no client dependency so users can use them even without an
# `OMOPHubClient` instance.


#' OMOPHub FHIR Terminology Service URL
#'
#' Convenience helper for constructing the OMOPHub FHIR Terminology
#' Service base URL for a given FHIR version. Use it when configuring
#' an external FHIR client library (`httr2`, `fhircrackr`, etc.) to
#' talk to OMOPHub's FHIR endpoint directly.
#'
#' @param version FHIR version prefix. One of `"r4"` (default),
#' `"r4b"`, `"r5"`, or `"r6"`.
#' @returns A character scalar with the full FHIR base URL, e.g.
#' `"https://fhir.omophub.com/fhir/r4"`.
#' @examples
#' omophub_fhir_url()
#' omophub_fhir_url("r5")
#'
#' \dontrun{
#' # Use with httr2 to call the $lookup operation directly
#' library(httr2)
#' req <- request(omophub_fhir_url()) |>
#' req_url_path_append("CodeSystem/$lookup") |>
#' req_url_query(
#' system = "http://snomed.info/sct",
#' code = "44054006"
#' ) |>
#' req_headers(Authorization = paste("Bearer", Sys.getenv("OMOPHUB_API_KEY")))
#' resp <- req_perform(req)
#' }
#' @export
omophub_fhir_url <- function(version = c("r4", "r4b", "r5", "r6")) {
version <- match.arg(version)
paste0("https://fhir.omophub.com/fhir/", version)
}
16 changes: 13 additions & 3 deletions man/FhirResource.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions man/fhir_batch_to_tibble.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading