This guide is for people extending or maintaining
nhsbsa. It explains how the package is put together and how
to add new functionality consistently. For using the package, see
vignette("nhsbsa") instead.
nhsbsa is a low-level, general-purpose
client for the NHS Business Services Authority Open Data
Portal, a CKAN catalogue. Two principles
shape every decision:
nhsbsa_list_resources() and
nhsbsa_download_resource(). They are “exceptions” because,
unlike every other exported function, neither corresponds to a single
CKAN action: nhsbsa_list_resources() calls
nhsbsa_package_show() and reshapes the nested
resources list into a tibble, and
nhsbsa_download_resource() resolves a resource (via
nhsbsa_package_show()) and then streams its file
url — an ordinary HTTP download that is not part of the
/action/ API at all. They earn their place because together
they are the operations a caller most often needs.The package is a thin shell around a single request layer.
endpoint functions -> nhsbsa_query() -> httr2 -> CKAN
(R/package.R, (R/core.R)
resources.R,
datastore.R,
catalogue.R)
R/core.R)nhsbsa_query(action) does all the work:
rlang::caller_fn() and rlang::fn_fmls_names(),
reads those argument values from the caller’s environment, and turns
them into query parameters. Arguments prefixed with . (such
as .return_raw) are reserved for client behaviour and never
sent to the API.nhsbsa_format_query_value(): NULLs are
dropped, logicals become "true"/"false",
multi-element vectors are joined with commas, and lists (such as
filters) are encoded as JSON.nhsbsa_perform() translates transport failures into a
graceful nhsbsa_offline error and HTTP errors into
nhsbsa_api_error (plus a status-specific subclass such as
nhsbsa_http_404).success = FALSE it
raises an nhsbsa_api_error; otherwise it returns the
result element (or, when the caller passed
.return_raw = TRUE, the whole parsed envelope).Because parameters are read from the function signature, an endpoint wrapper is usually a single line — see below.
R/conditions.R)All messages, warnings and errors go through
nhsbsa_abort(), nhsbsa_warn() and
nhsbsa_inform(). These wrap the corresponding
cli functions, always append a base class
(nhsbsa_error / nhsbsa_warning /
nhsbsa_message), and accept an optional class
to prepend a more specific subclass. Give a condition a specific
subclass whenever a caller might reasonably want to catch it, e.g.
Do not call cli::cli_abort() (or any other package’s
error helpers) directly.
package_show, package_search,
resource_show).package_list, organization_list,
group_list, tag_list).datastore_search, datastore_search_sql,
list_resources).nhsbsa_download_resource() returns the destination path
invisibly..return_raw = TRUE
returns the parsed envelope as a list.The portal exposes the standard CKAN read API (you can confirm what a
given instance supports with help_show, e.g.
https://opendata.nhsbsa.net/api/3/action/help_show?name=package_search).
The package wraps the useful read subset of those
actions — dataset discovery, resource listing/metadata/download,
datastore queries, and the catalogue listings. Other registered actions
(the *_autocomplete family, datastore_info,
license_list, status_show, and so on) are
intentionally not wrapped yet. If a gap is discovered, the convention is
to open an issue
and then add the wrapper as below.
To wrap another CKAN action, add a thin function whose arguments
mirror the API parameters. For example, to wrap
tag_show:
#' Show a tag
#'
#' Wraps the CKAN `tag_show` action.
#'
#' @param id Character scalar. The tag name or id.
#' @inheritParams nhsbsa_package_list
#'
#' @return A list of tag metadata.
#' @export
#' @examplesIf identical(Sys.getenv("IN_PKGDOWN"), "true")
#' nhsbsa_tag_show("prescribing")
nhsbsa_tag_show <- function(id, .return_raw = FALSE) {
nhsbsa_query("tag_show")
}That is the whole implementation: nhsbsa_query() picks
up id from the signature and sends it as a query parameter.
If the action returns tabular data, post-process the result with
nhsbsa_records_to_tibble().
Then:
Keep examples gated with
@examplesIf identical(Sys.getenv("IN_PKGDOWN"), "true") so
they render on the documentation site but never run on CRAN.
Tests use testthat (edition 3). The suite is designed to
run fully offline and deterministically; a small set of live tests is
gated separately.
Network calls are recorded as httptest2 fixtures and
replayed. Wrap a block in with_mock_dir(); the first run
records the responses, later runs replay them:
with_mock_dir("pl", {
test_that("nhsbsa_package_list returns dataset ids", {
out <- nhsbsa_package_list()
expect_type(out, "character")
})
})Keep the mock directory name short. The portal’s URL
path (opendata.nhsbsa.net/api/3/action/) is deep, and R CMD
check rejects tarball paths longer than 100 bytes, so a long directory
name plus a long action name can tip a fixture over the limit.
For error paths and edge cases it is simpler to mock the HTTP
response directly, without a fixture, using
httr2::with_mocked_responses():
test_that("an HTTP error surfaces as nhsbsa_api_error", {
resp <- httr2::response(
status_code = 404L,
headers = list(`Content-Type` = "application/json"),
body = charToRaw('{"success":false,"error":{"message":"nope"}}')
)
httr2::with_mocked_responses(function(req) resp, {
expect_error(nhsbsa_package_show("x"), class = "nhsbsa_http_404")
})
})Tests that contact the real API live in
tests/testthat/test-live.R. They are skipped on CRAN,
skipped when offline, and only run when the
NHSBSA_LIVE_TESTS environment variable is set. The
continuous integration workflows set this variable so the live tests run
there. Use live tests sparingly, as a smoke test that the real API still
behaves as expected.