From f0954a1660ce1dc7f3beabdb37370441f819abef Mon Sep 17 00:00:00 2001 From: Florian Mayer Date: Sun, 20 Oct 2024 16:09:50 +0800 Subject: [PATCH] WIP #152 * Add odata_entitylist_service_get --- DESCRIPTION | 2 +- NAMESPACE | 2 + R/odata_entitylist_service_get.R | 97 +++++++++++++ man/entity_audits.Rd | 3 +- man/entity_changes.Rd | 3 +- man/entity_create.Rd | 3 +- man/entity_delete.Rd | 3 +- man/entity_detail.Rd | 3 +- man/entity_list.Rd | 3 +- man/entity_update.Rd | 3 +- man/entity_versions.Rd | 3 +- man/entitylist_detail.Rd | 3 +- man/entitylist_download.Rd | 3 +- man/entitylist_list.Rd | 3 +- man/entitylist_update.Rd | 3 +- man/odata_entitylist_service_get.Rd | 132 ++++++++++++++++++ .../test-odata_entitylist_service_get.R | 105 ++++++++++++++ 17 files changed, 361 insertions(+), 13 deletions(-) create mode 100644 R/odata_entitylist_service_get.R create mode 100644 man/odata_entitylist_service_get.Rd create mode 100644 tests/testthat/test-odata_entitylist_service_get.R diff --git a/DESCRIPTION b/DESCRIPTION index d7191790..14d74347 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Type: Package Package: ruODK Title: An R Client for the ODK Central API -Version: 1.5.0 +Version: 1.5.0.9000 Authors@R:c( person(c("Florian", "W."), "Mayer", , "Florian.Mayer@dpc.wa.gov.au", role = c("aut", "cre"), comment = c(ORCID = "0000-0003-4269-4242")), diff --git a/NAMESPACE b/NAMESPACE index c843baa1..1ba65d38 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,5 +1,6 @@ # Generated by roxygen2: do not edit by hand +S3method(print,odata_entitylist_service_get) S3method(print,ru_settings) export("%>%") export(attachment_get) @@ -62,6 +63,7 @@ export(handle_ru_datetimes) export(handle_ru_geopoints) export(handle_ru_geoshapes) export(handle_ru_geotraces) +export(odata_entitylist_service_get) export(odata_metadata_get) export(odata_service_get) export(odata_submission_get) diff --git a/R/odata_entitylist_service_get.R b/R/odata_entitylist_service_get.R new file mode 100644 index 00000000..c5c35f08 --- /dev/null +++ b/R/odata_entitylist_service_get.R @@ -0,0 +1,97 @@ +#' Get the Service Document from the OData Dataset Service. +#' +#' `r lifecycle::badge("experimental")` +#' +#' ODK Central presents one OData service for every Dataset (Entity List) +#' as a way to get an OData feed of Entities. +#' To access the OData service, add `.svc` to the resource URL for the given +#' Dataset (Entity List). +#' +#' The Service Document provides a link to the main source of information +#' in this OData service: the list of Entities in this Dataset, +#' as well as the Metadata Document describing the schema of this information. +#' +#' This document is available only in JSON format. +#' +#' @template tpl-structure-nested +#' @template tpl-names-cleaned-top-level +#' @template tpl-def-entitylist +#' @template tpl-entitylist-dataset +#' @template tpl-auth-missing +#' @template tpl-compat-2022-3 +#' @template param-pid +#' @template param-did +#' @template param-url +#' @template param-auth +#' @template param-retries +#' @template param-odkcv +#' @template param-orders +#' @template param-tz +#' @return An S3 class `odata_entitylist_service_get` with two list items: +#' * `context` The URL for the OData metadata document +#' * `value` A tibble of EntitySets available in this EntityList +# nolint start +#' @seealso \url{https://docs.getodk.org/central-api-odata-endpoints/#odata-dataset-service} +# nolint end +#' @family entity-management +#' @export +#' @examples +#' \dontrun{ +#' # See vignette("setup") for setup and authentication options +#' # ruODK::ru_setup(svc = "....svc", un = "me@email.com", pw = "...") +#' +#' ds <- entitylist_list(pid = get_default_pid()) +#' +#' ds1 <- odata_entitylist_service_get(pid = get_default_pid(), did = ds$name[1]) +#' +#' ds1 +#' ds1$context +#' ds1$value +#' } +odata_entitylist_service_get <- function(pid = get_default_pid(), + did = "", + url = get_default_url(), + un = get_default_un(), + pw = get_default_pw(), + retries = get_retries(), + odkc_version = get_default_odkc_version(), + orders = get_default_orders(), + tz = get_default_tz()) { + yell_if_missing(url, un, pw, pid = pid, did = did) + + if (odkc_version |> semver_lt("2022.3")) { + ru_msg_warn("odata_entitylist_service_get is supported from v2022.3") + } + + ds <- httr::RETRY( + "GET", + httr::modify_url(url, + path = glue::glue( + "v1/projects/{pid}/datasets/", + "{URLencode(did, reserved = TRUE)}.svc" + ) + ), + httr::add_headers( + "Accept" = "application/json" + ), + httr::authenticate(un, pw), + times = retries + ) |> + yell_if_error(url, un, pw) |> + httr::content(encoding = "utf-8") |> + janitor::clean_names() + + structure(list( + context = ds$odata_context, + value = purrr::map_df(ds$value, ~ tibble::as_tibble(.x)) + ), class = "odata_entitylist_service_get") +} + +#' @export +print.odata_entitylist_service_get <- function(x, ...) { + cat("", sep = "\n") + cat(" OData Context: ", x$context, "\n") + cat(" OData Entities:", nrow(x$value), "\n") +} + +# usethis::use_test("odata_entitylist_service_get") # nolint diff --git a/man/entity_audits.Rd b/man/entity_audits.Rd index 4429f826..e85282c9 100644 --- a/man/entity_audits.Rd +++ b/man/entity_audits.Rd @@ -151,6 +151,7 @@ Other entity-management: \code{\link{entitylist_detail}()}, \code{\link{entitylist_download}()}, \code{\link{entitylist_list}()}, -\code{\link{entitylist_update}()} +\code{\link{entitylist_update}()}, +\code{\link{odata_entitylist_service_get}()} } \concept{entity-management} diff --git a/man/entity_changes.Rd b/man/entity_changes.Rd index 3f5a7cf1..1d68a6f4 100644 --- a/man/entity_changes.Rd +++ b/man/entity_changes.Rd @@ -150,6 +150,7 @@ Other entity-management: \code{\link{entitylist_detail}()}, \code{\link{entitylist_download}()}, \code{\link{entitylist_list}()}, -\code{\link{entitylist_update}()} +\code{\link{entitylist_update}()}, +\code{\link{odata_entitylist_service_get}()} } \concept{entity-management} diff --git a/man/entity_create.Rd b/man/entity_create.Rd index fe655acf..a8616dee 100644 --- a/man/entity_create.Rd +++ b/man/entity_create.Rd @@ -226,6 +226,7 @@ Other entity-management: \code{\link{entitylist_detail}()}, \code{\link{entitylist_download}()}, \code{\link{entitylist_list}()}, -\code{\link{entitylist_update}()} +\code{\link{entitylist_update}()}, +\code{\link{odata_entitylist_service_get}()} } \concept{entity-management} diff --git a/man/entity_delete.Rd b/man/entity_delete.Rd index b67be27e..b2729e1a 100644 --- a/man/entity_delete.Rd +++ b/man/entity_delete.Rd @@ -127,6 +127,7 @@ Other entity-management: \code{\link{entitylist_detail}()}, \code{\link{entitylist_download}()}, \code{\link{entitylist_list}()}, -\code{\link{entitylist_update}()} +\code{\link{entitylist_update}()}, +\code{\link{odata_entitylist_service_get}()} } \concept{entity-management} diff --git a/man/entity_detail.Rd b/man/entity_detail.Rd index f7ef608f..2f69e939 100644 --- a/man/entity_detail.Rd +++ b/man/entity_detail.Rd @@ -155,6 +155,7 @@ Other entity-management: \code{\link{entitylist_detail}()}, \code{\link{entitylist_download}()}, \code{\link{entitylist_list}()}, -\code{\link{entitylist_update}()} +\code{\link{entitylist_update}()}, +\code{\link{odata_entitylist_service_get}()} } \concept{entity-management} diff --git a/man/entity_list.Rd b/man/entity_list.Rd index f5a9bf25..9a02aaed 100644 --- a/man/entity_list.Rd +++ b/man/entity_list.Rd @@ -131,6 +131,7 @@ Other entity-management: \code{\link{entitylist_detail}()}, \code{\link{entitylist_download}()}, \code{\link{entitylist_list}()}, -\code{\link{entitylist_update}()} +\code{\link{entitylist_update}()}, +\code{\link{odata_entitylist_service_get}()} } \concept{entity-management} diff --git a/man/entity_update.Rd b/man/entity_update.Rd index ebd05b2f..7a5b618a 100644 --- a/man/entity_update.Rd +++ b/man/entity_update.Rd @@ -190,6 +190,7 @@ Other entity-management: \code{\link{entitylist_detail}()}, \code{\link{entitylist_download}()}, \code{\link{entitylist_list}()}, -\code{\link{entitylist_update}()} +\code{\link{entitylist_update}()}, +\code{\link{odata_entitylist_service_get}()} } \concept{entity-management} diff --git a/man/entity_versions.Rd b/man/entity_versions.Rd index 3a211853..8fbe3c6c 100644 --- a/man/entity_versions.Rd +++ b/man/entity_versions.Rd @@ -150,6 +150,7 @@ Other entity-management: \code{\link{entitylist_detail}()}, \code{\link{entitylist_download}()}, \code{\link{entitylist_list}()}, -\code{\link{entitylist_update}()} +\code{\link{entitylist_update}()}, +\code{\link{odata_entitylist_service_get}()} } \concept{entity-management} diff --git a/man/entitylist_detail.Rd b/man/entitylist_detail.Rd index fcd553a4..6db58e79 100644 --- a/man/entitylist_detail.Rd +++ b/man/entitylist_detail.Rd @@ -122,6 +122,7 @@ Other entity-management: \code{\link{entity_versions}()}, \code{\link{entitylist_download}()}, \code{\link{entitylist_list}()}, -\code{\link{entitylist_update}()} +\code{\link{entitylist_update}()}, +\code{\link{odata_entitylist_service_get}()} } \concept{entity-management} diff --git a/man/entitylist_download.Rd b/man/entitylist_download.Rd index 493e58ab..a7bac82d 100644 --- a/man/entitylist_download.Rd +++ b/man/entitylist_download.Rd @@ -206,6 +206,7 @@ Other entity-management: \code{\link{entity_versions}()}, \code{\link{entitylist_detail}()}, \code{\link{entitylist_list}()}, -\code{\link{entitylist_update}()} +\code{\link{entitylist_update}()}, +\code{\link{odata_entitylist_service_get}()} } \concept{entity-management} diff --git a/man/entitylist_list.Rd b/man/entitylist_list.Rd index c487433d..9467778d 100644 --- a/man/entitylist_list.Rd +++ b/man/entitylist_list.Rd @@ -107,6 +107,7 @@ Other entity-management: \code{\link{entity_versions}()}, \code{\link{entitylist_detail}()}, \code{\link{entitylist_download}()}, -\code{\link{entitylist_update}()} +\code{\link{entitylist_update}()}, +\code{\link{odata_entitylist_service_get}()} } \concept{entity-management} diff --git a/man/entitylist_update.Rd b/man/entitylist_update.Rd index 9b109b20..9c2ef4fb 100644 --- a/man/entitylist_update.Rd +++ b/man/entitylist_update.Rd @@ -137,6 +137,7 @@ Other entity-management: \code{\link{entity_versions}()}, \code{\link{entitylist_detail}()}, \code{\link{entitylist_download}()}, -\code{\link{entitylist_list}()} +\code{\link{entitylist_list}()}, +\code{\link{odata_entitylist_service_get}()} } \concept{entity-management} diff --git a/man/odata_entitylist_service_get.Rd b/man/odata_entitylist_service_get.Rd new file mode 100644 index 00000000..b8164e84 --- /dev/null +++ b/man/odata_entitylist_service_get.Rd @@ -0,0 +1,132 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/odata_entitylist_service_get.R +\name{odata_entitylist_service_get} +\alias{odata_entitylist_service_get} +\title{Get the Service Document from the OData Dataset Service.} +\usage{ +odata_entitylist_service_get( + pid = get_default_pid(), + did = "", + url = get_default_url(), + un = get_default_un(), + pw = get_default_pw(), + retries = get_retries(), + odkc_version = get_default_odkc_version(), + orders = get_default_orders(), + tz = get_default_tz() +) +} +\arguments{ +\item{pid}{The numeric ID of the project, e.g.: 2. + +Default: \code{\link{get_default_pid}}. + +Set default \code{pid} through \code{ru_setup(pid="...")}. + +See \code{vignette("Setup", package = "ruODK")}.} + +\item{did}{(chr) The name of the Entity List, internally called Dataset. +The function will error if this parameter is not given. +Default: "".} + +\item{url}{The ODK Central base URL without trailing slash. + +Default: \code{\link{get_default_url}}. + +Set default \code{url} through \code{ru_setup(url="...")}. + +See \code{vignette("Setup", package = "ruODK")}.} + +\item{un}{The ODK Central username (an email address). +Default: \code{\link{get_default_un}}. +Set default \code{un} through \code{ru_setup(un="...")}. +See \code{vignette("Setup", package = "ruODK")}.} + +\item{pw}{The ODK Central password. +Default: \code{\link{get_default_pw}}. +Set default \code{pw} through \code{ru_setup(pw="...")}. +See \code{vignette("Setup", package = "ruODK")}.} + +\item{retries}{The number of attempts to retrieve a web resource. + +This parameter is given to \code{\link[httr]{RETRY}(times = retries)}. + +Default: 3.} + +\item{odkc_version}{The ODK Central version as a semantic version string +(year.minor.patch), e.g. "2023.5.1". The version is shown on ODK Central's +version page \verb{/version.txt}. Discard the "v". +\code{ruODK} uses this parameter to adjust for breaking changes in ODK Central. + +Default: \code{\link{get_default_odkc_version}} or "2023.5.1" if unset. + +Set default \code{get_default_odkc_version} through +\code{ru_setup(odkc_version="2023.5.1")}. + +See \code{vignette("Setup", package = "ruODK")}.} + +\item{orders}{(vector of character) Orders of datetime elements for +lubridate. + +Default: +\code{c("YmdHMS", "YmdHMSz", "Ymd HMS", "Ymd HMSz", "Ymd", "ymd")}.} + +\item{tz}{A timezone to convert dates and times to. + +Read \code{vignette("setup", package = "ruODK")} to learn how \code{ruODK}'s +timezone can be set globally or per function.} +} +\value{ +An S3 class \code{odata_entitylist_service_get} with two list items: +\itemize{ +\item \code{context} The URL for the OData metadata document +\item \code{value} A tibble of EntitySets available in this EntityList +} +} +\description{ +\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#experimental}{\figure{lifecycle-experimental.svg}{options: alt='[Experimental]'}}}{\strong{[Experimental]}} +} +\details{ +ODK Central presents one OData service for every Dataset (Entity List) +as a way to get an OData feed of Entities. +To access the OData service, add \code{.svc} to the resource URL for the given +Dataset (Entity List). + +The Service Document provides a link to the main source of information +in this OData service: the list of Entities in this Dataset, +as well as the Metadata Document describing the schema of this information. + +This document is available only in JSON format. +} +\examples{ +\dontrun{ +# See vignette("setup") for setup and authentication options +# ruODK::ru_setup(svc = "....svc", un = "me@email.com", pw = "...") + +ds <- entitylist_list(pid = get_default_pid()) + +ds1 <- odata_entitylist_service_get(pid = get_default_pid(), did = ds$name[1]) + +ds1 +ds1$context +ds1$value +} +} +\seealso{ +\url{https://docs.getodk.org/central-api-odata-endpoints/#odata-dataset-service} + +Other entity-management: +\code{\link{entity_audits}()}, +\code{\link{entity_changes}()}, +\code{\link{entity_create}()}, +\code{\link{entity_delete}()}, +\code{\link{entity_detail}()}, +\code{\link{entity_list}()}, +\code{\link{entity_update}()}, +\code{\link{entity_versions}()}, +\code{\link{entitylist_detail}()}, +\code{\link{entitylist_download}()}, +\code{\link{entitylist_list}()}, +\code{\link{entitylist_update}()} +} +\concept{entity-management} diff --git a/tests/testthat/test-odata_entitylist_service_get.R b/tests/testthat/test-odata_entitylist_service_get.R new file mode 100644 index 00000000..0317a6a2 --- /dev/null +++ b/tests/testthat/test-odata_entitylist_service_get.R @@ -0,0 +1,105 @@ +# Test the odata_entitylist_service_get function +test_that("odata_entitylist_service_get works correctly with valid inputs", { + skip_if(Sys.getenv("ODKC_TEST_URL") == "", + message = "Test server not configured" + ) + + ru_setup( + pid = get_test_pid(), + url = get_test_url(), + un = get_test_un(), + pw = get_test_pw(), + odkc_version = get_test_odkc_version() + ) + + ds <- entitylist_list() + + ds1 <- odata_entitylist_service_get(did = ds$name[1]) + + ctx <- as.character(glue::glue( + "{get_test_url()}/v1/projects/{get_test_pid()}/", + "datasets/{ds$name[1]}.svc/$metadata" + )) + + # Check the structure of the result + testthat::expect_s3_class(ds1, "odata_entitylist_service_get") + expect_equal( + ds1$context, + ctx + ) + testthat::expect_equal(nrow(ds1$value), 1) + testthat::expect_equal(ds1$value$name[1], "Entities") +}) + +test_that("odata_entitylist_service_get print works", { + skip_if(Sys.getenv("ODKC_TEST_URL") == "", + message = "Test server not configured" + ) + + ru_setup( + pid = get_test_pid(), + url = get_test_url(), + un = get_test_un(), + pw = get_test_pw(), + odkc_version = get_test_odkc_version() + ) + + ds <- entitylist_list() + + ds1 <- odata_entitylist_service_get(did = ds$name[1]) + + # Test print + out <- testthat::capture_output(print(ds1)) + testthat::expect_true(any(grepl("", out))) + testthat::expect_true(any(grepl(glue::glue("OData Context"), out))) + testthat::expect_true(any(grepl(glue::glue("OData Entities"), out))) +}) + + +test_that("odata_entitylist_service_get warns on missing arguments", { + skip_if(Sys.getenv("ODKC_TEST_URL") == "", + message = "Test server not configured" + ) + + ru_setup( + pid = get_test_pid(), + url = get_test_url(), + un = get_test_un(), + pw = get_test_pw(), + odkc_version = get_test_odkc_version() + ) + + testthat::expect_error( + odata_entitylist_service_get(pid = "", did = "") + ) + + testthat::expect_error( + odata_entitylist_service_get(pid = get_test_pid(), did = "") + ) +}) + + +test_that("entitylist_detail warns if odkc_version too low", { + skip_if(Sys.getenv("ODKC_TEST_URL") == "", + message = "Test server not configured" + ) + + ru_setup( + pid = get_test_pid(), + url = get_test_url(), + un = get_test_un(), + pw = get_test_pw(), + odkc_version = get_test_odkc_version() + ) + + ds <- entitylist_list() + did <- ds$name[1] + + ds1 <- odata_entitylist_service_get(did = did) + + testthat::expect_warning( + ds1 <- entitylist_detail(did = did, odkc_version = "1.5.3") + ) +}) + +# usethis::use_r("odata_entitylist_service_get") # nolint