Skip to content

Commit e8bb9b2

Browse files
authored
Merge pull request #3 from OMOPHub/develop
Release version 1.7.0 with new FHIR resolution features
2 parents 2c18aae + 7b55688 commit e8bb9b2

14 files changed

Lines changed: 661 additions & 8 deletions

DESCRIPTION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Package: omophub
22
Title: R Client for the 'OMOPHub' Medical Vocabulary API
3-
Version: 1.6.0
3+
Version: 1.7.0
44
Authors@R: c(
55
person("Alex", "Chen", email = "alex@omophub.com", role = c("aut", "cre", "cph")),
66
person("Observational Health Data Science and Informatics", role = c("cph"))

NAMESPACE

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@ S3method(print,omophub_mappings)
1111
S3method(print,omophub_relationships)
1212
S3method(print,omophub_vocabularies)
1313
export(OMOPHubClient)
14+
export(fhir_resolve)
15+
export(fhir_resolve_batch)
16+
export(fhir_resolve_codeable_concept)
1417
export(get_api_key)
1518
export(has_api_key)
19+
export(omophub_fhir_url)
1620
export(set_api_key)
1721
importFrom(R6,R6Class)
1822
importFrom(cli,cli_abort)

NEWS.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,34 @@
1+
# omophub 1.7.0
2+
3+
## New Features
4+
5+
* **Tibble output for batch FHIR resolution**
6+
`FhirResource$resolve_batch()` (and the standalone `fhir_resolve_batch()`)
7+
gained an `as_tibble` parameter. When `as_tibble = TRUE`, the call returns
8+
a flat `tibble::tibble` with one row per input coding and columns for the
9+
source concept, standard concept, target CDM table, mapping type, and
10+
resolution status - ready to pipe into `dplyr` / `tidyr`. The batch
11+
summary (`total` / `resolved` / `failed`) is attached as
12+
`attr(result, "summary")`. Default behavior is unchanged: `as_tibble = FALSE`
13+
still returns the legacy list shape.
14+
15+
* **Standalone wrapper functions**
16+
Thin, pipe-friendly wrappers around the R6 `FhirResource` methods:
17+
18+
- `fhir_resolve(client, ...)`
19+
- `fhir_resolve_batch(client, ...)`
20+
- `fhir_resolve_codeable_concept(client, ...)`
21+
22+
Both the R6 form (`client$fhir$resolve(...)`) and the standalone form
23+
work; pick whichever reads better in your pipeline.
24+
25+
* **FHIR client interop helper**
26+
New exported function `omophub_fhir_url(version)` returning the OMOPHub
27+
FHIR Terminology Service base URL for a given FHIR version (`"r4"`,
28+
`"r4b"`, `"r5"`, `"r6"`). Use it with `httr2`, `fhircrackr`, or any
29+
external FHIR client that wants to talk to OMOPHub's FHIR endpoint
30+
directly.
31+
132
# omophub 1.6.0
233

334
## New Features

R/fhir.R

Lines changed: 184 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,23 @@ FhirResource <- R6::R6Class(
8383
#' @param include_recommendations Logical. Default `FALSE`.
8484
#' @param recommendations_limit Integer. Default `5L`.
8585
#' @param include_quality Logical. Default `FALSE`.
86+
#' @param as_tibble Logical. When `TRUE`, returns a [tibble::tibble]
87+
#' with one row per input coding and flat columns for the source
88+
#' concept, standard concept, target CDM table, mapping type, and
89+
#' resolution status. The batch `summary` (total/resolved/failed)
90+
#' is attached as `attr(result, "summary")`. Default `FALSE`
91+
#' keeps the legacy list-shaped return.
8692
#'
87-
#' @returns A list with `results` (per-item) and `summary`
88-
#' (total/resolved/failed).
93+
#' @returns When `as_tibble = FALSE` (default), a list with `results`
94+
#' (per-item) and `summary` (total/resolved/failed). When
95+
#' `as_tibble = TRUE`, a [tibble::tibble] suitable for
96+
#' `dplyr`/`tidyr` pipelines.
8997
resolve_batch = function(codings,
9098
resource_type = NULL,
9199
include_recommendations = FALSE,
92100
recommendations_limit = 5L,
93-
include_quality = FALSE) {
101+
include_quality = FALSE,
102+
as_tibble = FALSE) {
94103
stopifnot(is.list(codings), length(codings) >= 1, length(codings) <= 100)
95104
if (!all(vapply(codings, is.list, logical(1)))) {
96105
cli::cli_abort(c(
@@ -108,7 +117,12 @@ FhirResource <- R6::R6Class(
108117
}
109118
if (isTRUE(include_quality)) body$include_quality <- TRUE
110119

111-
perform_post(private$.base_req, "fhir/resolve/batch", body = body)
120+
result <- perform_post(private$.base_req, "fhir/resolve/batch", body = body)
121+
122+
if (isTRUE(as_tibble)) {
123+
return(fhir_batch_to_tibble(result, codings))
124+
}
125+
result
112126
},
113127

114128
#' @description
@@ -168,3 +182,169 @@ compact_list <- function(...) {
168182
args <- list(...)
169183
args[!vapply(args, is.null, logical(1))]
170184
}
185+
186+
187+
#' Flatten a batch resolver response into a tibble.
188+
#'
189+
#' Internal helper used by `FhirResource$resolve_batch(as_tibble = TRUE)`.
190+
#' Produces one row per input coding with flat columns for the source
191+
#' concept, standard concept, target CDM table, and status. The original
192+
#' `summary` list is attached to the returned tibble via
193+
#' `attr(x, "summary")`.
194+
#'
195+
#' @param result The raw list result from `perform_post`.
196+
#' @param codings The original input `codings` list, used to align rows
197+
#' when items fail resolution and no `source_concept` is returned.
198+
#' @returns A [tibble::tibble].
199+
#' @keywords internal
200+
fhir_batch_to_tibble <- function(result, codings) {
201+
items <- result$results %||% list()
202+
n <- length(codings)
203+
204+
make_row <- function(i) {
205+
input_coding <- codings[[i]]
206+
item <- if (i <= length(items)) items[[i]] else NULL
207+
208+
# Failed items: the API returns them without a resolution block.
209+
resolution <- item$resolution
210+
if (is.null(resolution)) {
211+
return(tibble::tibble(
212+
source_system = input_coding$system %||% NA_character_,
213+
source_code = input_coding$code %||% NA_character_,
214+
source_concept_id = NA_integer_,
215+
source_concept_name = NA_character_,
216+
standard_concept_id = NA_integer_,
217+
standard_concept_name = NA_character_,
218+
standard_vocabulary_id = NA_character_,
219+
domain_id = NA_character_,
220+
target_table = NA_character_,
221+
mapping_type = NA_character_,
222+
similarity_score = NA_real_,
223+
status = "failed",
224+
status_detail = item$error %||% "unresolved"
225+
))
226+
}
227+
228+
src <- resolution$source_concept %||% list()
229+
std <- resolution$standard_concept %||% list()
230+
231+
tibble::tibble(
232+
source_system = input_coding$system %||% NA_character_,
233+
source_code = input_coding$code %||% NA_character_,
234+
source_concept_id = src$concept_id %||% NA_integer_,
235+
source_concept_name = src$concept_name %||% NA_character_,
236+
standard_concept_id = std$concept_id %||% NA_integer_,
237+
standard_concept_name = std$concept_name %||% NA_character_,
238+
standard_vocabulary_id = std$vocabulary_id %||% NA_character_,
239+
domain_id = std$domain_id %||% NA_character_,
240+
target_table = resolution$target_table %||% NA_character_,
241+
mapping_type = resolution$mapping_type %||% NA_character_,
242+
similarity_score = resolution$similarity_score %||% NA_real_,
243+
status = "resolved",
244+
status_detail = NA_character_
245+
)
246+
}
247+
248+
rows <- lapply(seq_len(n), make_row)
249+
tbl <- do.call(rbind, rows)
250+
attr(tbl, "summary") <- result$summary
251+
tbl
252+
}
253+
254+
255+
#' Null-coalescing operator.
256+
#' @noRd
257+
`%||%` <- function(a, b) if (is.null(a)) b else a
258+
259+
260+
# =============================================================================
261+
# Standalone wrapper functions
262+
#
263+
# Thin wrappers around the R6 `FhirResource` methods so users can write
264+
# pipe-friendly code without dereferencing the client every call:
265+
#
266+
# client |> fhir_resolve(system = "http://snomed.info/sct", code = "44054006")
267+
#
268+
# These forward all arguments to the underlying R6 method.
269+
# =============================================================================
270+
271+
#' Resolve a FHIR Coding to an OMOP standard concept
272+
#'
273+
#' Standalone wrapper for `client$fhir$resolve()`. See
274+
#' [FhirResource] for full parameter documentation.
275+
#'
276+
#' @param client An [OMOPHubClient] instance.
277+
#' @param ... Arguments passed to `FhirResource$resolve()`.
278+
#' @returns A list with `input` and `resolution`.
279+
#' @seealso [FhirResource], [fhir_resolve_batch()],
280+
#' [fhir_resolve_codeable_concept()]
281+
#' @examples
282+
#' \dontrun{
283+
#' client <- OMOPHubClient$new(api_key = Sys.getenv("OMOPHUB_API_KEY"))
284+
#' fhir_resolve(
285+
#' client,
286+
#' system = "http://snomed.info/sct",
287+
#' code = "44054006",
288+
#' resource_type = "Condition"
289+
#' )
290+
#' }
291+
#' @export
292+
fhir_resolve <- function(client, ...) {
293+
client$fhir$resolve(...)
294+
}
295+
296+
297+
#' Batch-resolve FHIR Codings
298+
#'
299+
#' Standalone wrapper for `client$fhir$resolve_batch()`. When
300+
#' `as_tibble = TRUE`, the result is a flat `tibble` suitable for
301+
#' `dplyr`/`tidyr` pipelines.
302+
#'
303+
#' @param client An [OMOPHubClient] instance.
304+
#' @param ... Arguments passed to `FhirResource$resolve_batch()`.
305+
#' @returns See `FhirResource$resolve_batch()`.
306+
#' @seealso [FhirResource], [fhir_resolve()],
307+
#' [fhir_resolve_codeable_concept()]
308+
#' @examples
309+
#' \dontrun{
310+
#' client <- OMOPHubClient$new(api_key = Sys.getenv("OMOPHUB_API_KEY"))
311+
#' tbl <- fhir_resolve_batch(
312+
#' client,
313+
#' codings = list(
314+
#' list(system = "http://snomed.info/sct", code = "44054006"),
315+
#' list(system = "http://loinc.org", code = "2339-0")
316+
#' ),
317+
#' as_tibble = TRUE
318+
#' )
319+
#' dplyr::filter(tbl, status == "resolved")
320+
#' }
321+
#' @export
322+
fhir_resolve_batch <- function(client, ...) {
323+
client$fhir$resolve_batch(...)
324+
}
325+
326+
327+
#' Resolve a FHIR CodeableConcept with vocabulary preference
328+
#'
329+
#' Standalone wrapper for `client$fhir$resolve_codeable_concept()`.
330+
#'
331+
#' @param client An [OMOPHubClient] instance.
332+
#' @param ... Arguments passed to `FhirResource$resolve_codeable_concept()`.
333+
#' @returns See `FhirResource$resolve_codeable_concept()`.
334+
#' @seealso [FhirResource], [fhir_resolve()], [fhir_resolve_batch()]
335+
#' @examples
336+
#' \dontrun{
337+
#' client <- OMOPHubClient$new(api_key = Sys.getenv("OMOPHUB_API_KEY"))
338+
#' fhir_resolve_codeable_concept(
339+
#' client,
340+
#' coding = list(
341+
#' list(system = "http://snomed.info/sct", code = "44054006"),
342+
#' list(system = "http://hl7.org/fhir/sid/icd-10-cm", code = "E11.9")
343+
#' ),
344+
#' resource_type = "Condition"
345+
#' )
346+
#' }
347+
#' @export
348+
fhir_resolve_codeable_concept <- function(client, ...) {
349+
client$fhir$resolve_codeable_concept(...)
350+
}

R/fhir_interop.R

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# FHIR client interop helpers for the OMOPHub FHIR Terminology Service.
2+
#
3+
# These helpers make it easy to configure an external FHIR client (like
4+
# `httr2`, `fhircrackr`, or a custom Spring Security OAuth2 client) to
5+
# talk directly to OMOPHub's FHIR endpoint. They are intentionally thin
6+
# and have no client dependency so users can use them even without an
7+
# `OMOPHubClient` instance.
8+
9+
10+
#' OMOPHub FHIR Terminology Service URL
11+
#'
12+
#' Convenience helper for constructing the OMOPHub FHIR Terminology
13+
#' Service base URL for a given FHIR version. Use it when configuring
14+
#' an external FHIR client library (`httr2`, `fhircrackr`, etc.) to
15+
#' talk to OMOPHub's FHIR endpoint directly.
16+
#'
17+
#' @param version FHIR version prefix. One of `"r4"` (default),
18+
#' `"r4b"`, `"r5"`, or `"r6"`.
19+
#' @returns A character scalar with the full FHIR base URL, e.g.
20+
#' `"https://fhir.omophub.com/fhir/r4"`.
21+
#' @examples
22+
#' omophub_fhir_url()
23+
#' omophub_fhir_url("r5")
24+
#'
25+
#' \dontrun{
26+
#' # Use with httr2 to call the $lookup operation directly
27+
#' library(httr2)
28+
#' req <- request(omophub_fhir_url()) |>
29+
#' req_url_path_append("CodeSystem/$lookup") |>
30+
#' req_url_query(
31+
#' system = "http://snomed.info/sct",
32+
#' code = "44054006"
33+
#' ) |>
34+
#' req_headers(Authorization = paste("Bearer", Sys.getenv("OMOPHUB_API_KEY")))
35+
#' resp <- req_perform(req)
36+
#' }
37+
#' @export
38+
omophub_fhir_url <- function(version = c("r4", "r4b", "r5", "r6")) {
39+
version <- match.arg(version)
40+
paste0("https://fhir.omophub.com/fhir/", version)
41+
}

man/FhirResource.Rd

Lines changed: 13 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/fhir_batch_to_tibble.Rd

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)