diff --git a/DESCRIPTION b/DESCRIPTION index 00c31b4..ff2a36a 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: kbtbr Type: Package Title: A Wrapper for the KoBoToolbox API -Version: 0.1.0.9000 +Version: 0.1.0.9002 Authors@R: c(person("Dimitri", "Marinelli", role = c("aut")), person("Lada", "Rudnitckaia", role = "aut"), person("Malte", "Kyhos", role = "aut"), diff --git a/R/kobo-paginator.R b/R/kobo-paginator.R new file mode 100644 index 0000000..c8d082c --- /dev/null +++ b/R/kobo-paginator.R @@ -0,0 +1,83 @@ +#' @title KoboPaginator +#' @description +#' A class that implements link-header style pagination, as is used in +#' the Kobotoolbox API. +#' @export +KoboPaginator <- R6::R6Class( + public = list( + client = NULL, + #' @description + #' @param client KoboClient. An instance of a KoboClient that can + #' be used for the paginated requestes. + initialize = function(client) { + private$client <- assert_class(client, "KoboClient") + }, + #' @description + #' @param path + #' @param query + #' @param ... Additional parameters passed to internally + #' called `KoboClient$get()`. + get = function(path, query, ...) { + assert_string(path) + assert_string(query) + private$page(path, query, ...) + }, + #' @description + #' Set the initial response + #' @details + #' Usually, the page method would revoke a first response in the + #' normal way, using its `next` element to walk over all subsequent + #' pages. In some settings, the user might provide this initial response + #' object already. + set_first_response = function(response, force_reset = FALSE) { + assert_list(response) + if (!(force_reset || is.null(private$resps))) { + stop("There are already existing responses. Use 'force_reset = TRUE' to reset/delete them.") + } + private$resps <- list(response) + }, + get_responses = function() { + return(private$resps) + } + ), + private = list( + resps = NULL, + page = function(path, query, sleep = 0.5, ...) { + tmp <- list() + + # Retrieve initial response + + # Initialize Pagination + tmp[[1]] <- private$resps[[1]] %||% self$client$get(path, query, ...) + next_link <- tmp[[1]][["next"]] + cnt <- 1 + + repeat { + if (is.null(next_link)) { + message(sprintf("Iterated over %s pages.", cnt)) + break + } + + # New iteration + Sys.sleep(sleep) + cnt <- cnt + 1 + tmp_path <- private$resolve_next(next_link) + + tmp[[cnt]] <- self$client$get(tmp_path, query, ...) + tmp[[cnt]]$raise_for_status() + next_link <- tmp[[cnt]][["next"]] + } + + private$resps <- tmp + }, + resolve_next = function(link) { + # Subtract base path etc. from + resolved <- gsub( + pattern = paste0("^", self$client$get_base_url()), + replacement = "", + x = link + ) + return(resolved) + } + ) +) diff --git a/R/kobo.R b/R/kobo.R index 2843652..d6b292d 100644 --- a/R/kobo.R +++ b/R/kobo.R @@ -6,15 +6,13 @@ #' interactions with the various endpoints. #' @export Kobo <- R6::R6Class("Kobo", - # private = list( - # ), public = list( # Public Fields ============================================================ #' @field session_v2 [kbtbr::KoboClient] session for v2 of the API session_v2 = NULL, - #' @field session_v1 `KoboClient` session for v1 of the API + #' @field session_v1 [KoboClient] session for v1 of the API session_v1 = NULL, # Public Methods =========================================================== @@ -28,9 +26,9 @@ Kobo <- R6::R6Class("Kobo", #' For example: https://kc.correlaid.org. #' @param kobo_token character. The API token. Defaults to requesting #' the systen environment `KBTBR_TOKEN`. - #' @param session_v2 [KoboClient] To pass directly + #' @param session_v2 [KoboClient]. Alternatively, pass directly #' a [KoboClient] instance for the API version v2. - #' @param session_v1 [KoboClient] In addition to session_v2 one can pass + #' @param session_v1 KoboClient. In addition to session_v2 one can pass #' also a [KoboClient] instance for the API version v1. initialize = function(base_url_v2 = NULL, base_url_v1 = NULL, kobo_token = Sys.getenv("KBTBR_TOKEN"), @@ -68,7 +66,7 @@ Kobo <- R6::R6Class("Kobo", #' @param format character. the format to request from the server. either 'json' or 'csv'. defaults to 'json' #' @param parse whether or not to parse the HTTP response. defaults to TRUE. #' @return a list encoding of the json server reply if parse=TRUE. - #' Otherwise, it returns the server response as a crul::HttpResponse + #' Otherwise, it returns the server response as a [crul::HttpResponse] #' object. get = function(path, query = list(), version = "v2", format = "json", parse = TRUE) { @@ -107,9 +105,12 @@ Kobo <- R6::R6Class("Kobo", ) } + # Select client + obj <- private$select_prep_client(path, version) + res <- obj$client$get(obj$path, query) res$raise_for_status() - if (format == "json" & parse) { + if (parse && format == "json") { res$raise_for_ct_json() return(res$parse("UTF-8") %>% jsonlite::fromJSON()) } else if (format == "csv" & parse) { @@ -123,6 +124,13 @@ Kobo <- R6::R6Class("Kobo", } return(res) }, + get_paginated = function(path, query, version = "v2", + format = "json", + parse = TRUE) { + obj <- private$select_prep_client(path, version) + paginator <- KoboPaginator$new(client = obj$client) + res <- paginator$get(path, query) + }, #' @description #' Wrapper for the POST method of internal session objects. @@ -302,5 +310,35 @@ Kobo <- R6::R6Class("Kobo", ) self$post("assets/", body = body) } - ) # + ), # + private = list( + + # Private Methods ========================================================== + + #' @description + #' Logic to select and prepare a client + select_prep_client = function(path, version) { + checkmate::assert_choice(version, c("v1", "v2")) + + if (version == "v2") { + obj <- list( + client = self$session_v2, + path = paste0("api/v2/", path) + ) + } else if (version == "v1") { + if (checkmate::test_null(self$session_v1)) { + usethis::ui_stop(paste( + "Session for API v1 is not initalized.", + "Please re-initalize the Kobo client with the", + "base_url_v1 argument." + )) + } + obj <- list( + client = self$session_v1, + path = paste0("api/v1/", path) + ) + } + return(obj) + } + ) ) diff --git a/man/Kobo.Rd b/man/Kobo.Rd index 306440a..443acf1 100644 --- a/man/Kobo.Rd +++ b/man/Kobo.Rd @@ -14,7 +14,7 @@ interactions with the various endpoints. \describe{ \item{\code{session_v2}}{\link{KoboClient} session for v2 of the API} -\item{\code{session_v1}}{\code{KoboClient} session for v1 of the API} +\item{\code{session_v1}}{\link{KoboClient} session for v1 of the API} } \if{html}{\out{}} } @@ -23,6 +23,7 @@ interactions with the various endpoints. \itemize{ \item \href{#method-new}{\code{Kobo$new()}} \item \href{#method-get}{\code{Kobo$get()}} +\item \href{#method-get_paginated}{\code{Kobo$get_paginated()}} \item \href{#method-post}{\code{Kobo$post()}} \item \href{#method-get_assets}{\code{Kobo$get_assets()}} \item \href{#method-get_surveys}{\code{Kobo$get_surveys()}} @@ -63,10 +64,10 @@ For example: https://kc.correlaid.org.} \item{\code{kobo_token}}{character. The API token. Defaults to requesting the systen environment \code{KBTBR_TOKEN}.} -\item{\code{session_v2}}{\link{KoboClient} To pass directly +\item{\code{session_v2}}{\link{KoboClient}. Alternatively, pass directly a \link{KoboClient} instance for the API version v2.} -\item{\code{session_v1}}{\link{KoboClient} In addition to session_v2 one can pass +\item{\code{session_v1}}{KoboKlient. In addition to session_v2 one can pass also a \link{KoboClient} instance for the API version v1.} } \if{html}{\out{}} @@ -99,9 +100,18 @@ component. The order is not hierarchical.} } \subsection{Returns}{ a list encoding of the json server reply if parse=TRUE. -Otherwise, it returns the server response as a crul::HttpResponse +Otherwise, it returns the server response as a \link[crul:HttpResponse]{crul::HttpResponse} object. } +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-get_paginated}{}}} +\subsection{Method \code{get_paginated()}}{ +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{Kobo$get_paginated(path, query, version = "v2", format = "json", parse = TRUE)}\if{html}{\out{
}} +} + } \if{html}{\out{
}} \if{html}{\out{}} diff --git a/man/KoboPaginator.Rd b/man/KoboPaginator.Rd new file mode 100644 index 0000000..2903e4d --- /dev/null +++ b/man/KoboPaginator.Rd @@ -0,0 +1,89 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/kobo-paginator.R +\name{KoboPaginator} +\alias{KoboPaginator} +\title{KoboPaginator} +\description{ +A class that implements link-header style pagination, as is used in +the Kobotoolbox API. +} +\section{Methods}{ +\subsection{Public methods}{ +\itemize{ +\item \href{#method-new}{\code{KoboPaginator$new()}} +\item \href{#method-get}{\code{KoboPaginator$get()}} +\item \href{#method-set_first_response}{\code{KoboPaginator$set_first_response()}} +\item \href{#method-get_responses}{\code{KoboPaginator$get_responses()}} +\item \href{#method-clone}{\code{KoboPaginator$clone()}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-new}{}}} +\subsection{Method \code{new()}}{ +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{KoboPaginator$new(client)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{client}}{KoboClient. An instance of a KoboClient that can +be used for the paginated requestes.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-get}{}}} +\subsection{Method \code{get()}}{ +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{KoboPaginator$get(path, query, ...)}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-set_first_response}{}}} +\subsection{Method \code{set_first_response()}}{ +Set the initial response +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{KoboPaginator$set_first_response(response)}\if{html}{\out{
}} +} + +\subsection{Details}{ +Usually, the page method would revoke a first response in the +normal way, using its \code{next} element to walk over all subsequent +pages. In some settings, the user might provide this initial response +object already. +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-get_responses}{}}} +\subsection{Method \code{get_responses()}}{ +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{KoboPaginator$get_responses()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-clone}{}}} +\subsection{Method \code{clone()}}{ +The objects of this class are cloneable with this method. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{KoboPaginator$clone(deep = FALSE)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{deep}}{Whether to make a deep clone.} +} +\if{html}{\out{
}} +} +} +} diff --git a/tests/testthat/test-kobo.R b/tests/testthat/test-kobo.R index aadeba2..d73e1f6 100644 --- a/tests/testthat/test-kobo.R +++ b/tests/testthat/test-kobo.R @@ -118,7 +118,9 @@ test_that("Kobo can get submissions for a survey", { expect_true(tibble::is_tibble(response_df)) expect_equal(nrow(response_df), 4) }) + # ERRORS ----------- + vcr::use_cassette("kobo-get-404", { test_that("non existing route throws 404 error", { kobo <- Kobo$new(base_url_v2 = BASE_URL, kobo_token = Sys.getenv("KBTBR_TOKEN"))