Below we have written an example aggregate-type client-side function. It obtains the levels of a server-side factor variable, performs some disclosure checks and returns the levels along with a message. We then break down each part of the code to explain what it is doing.
#' @title Returns levels of a factor variable with fun message
#' @description Retrieves the number of columns in a dataset and provides a motivational message
#' @param x a character string providing the name of the input data frame or matrix.
#' @param fun_message a character string with the motivational message
#' @param datasources a list of \code{\link{DSConnection-class}} objects obtained after login.
#' If the \code{datasources} argument is not specified
#' the default set of connections will be used: see \code{\link{datashield.connections_default}}.
#' @return \code{ds.funLevels} returns the levels prefixed by a fun message
#' @author Tim Cadman
#' @importFrom DSI datashield.aggregate
#' @examples
#' @export
ds.funLevels <- function(x=NULL, fun_message=NULL, datasources=NULL) {
if(is.null(datasources)){
datasources <- datashield.connections_find()
}
.check_args(x, fun_message)
cally <- call("funLevelsDS", x, fun_message)
fun_levels <- DSI::datashield.aggregate(datasources, cally)
return(fun_levels)
}
#' Validate Arguments
#'
#' This function checks whether the provided arguments meet the expected conditions.
#' It ensures that `x` and `fun_message` are not `NULL` and that both are character vectors.
#'
#' @param x A character vector. Must not be `NULL`.
#' @param fun_message A character vector. Must not be `NULL`.
#'
#' @return This function does not return a value. It throws an error if any of the conditions are not met.
#'
#' @importFrom cli cli_abort
#'
#' @examples
#' # Valid input
#' .check_args("example", "This is a message")
#'
#' # Invalid input (NULL x)
#' # .check_args(NULL, "This is a message")
#'
#' # Invalid input (x not character)
#' # .check_args(123, "This is a message")
#'
.check_args <- function(x, fun_message) {
if(is.null(x)) {
cli_abort("Argument 'x' must not be NULL")
}
if(is.null(message)) {
cli_abort("Argument 'message' must not be NULL")
}
x_class <- class(x)
message_class <- class(fun_message)
if(x_class != "character") {
cli_abort(
c(
"x" = "`x` must be a character vector",
"i" = "You have provided an input with class {x_class}")
)
}
if(message_class != "character") {
cli_abort(
c(
"x" = "`fun_message` must be a character vector",
"i" = "You have provided an input with class {message_class}")
)
}
}
This is a section of roxygen2 comments providing meta-data about the function. These comments will be later used to build the documentation for your package.
#' @title Returns levels of a factor variable with fun message
#' @description Retrieves the number of columns in a dataset and provides a motivational message
#' @param x a character string providing the name of the input data frame or matrix.
#' @param fun_message a character string with the motivational message
#' @param datasources a list of \code{\link{DSConnection-class}} objects obtained after login.
#' If the \code{datasources} argument is not specified
#' the default set of connections will be used: see \code{\link{datashield.connections_default}}.
#' @return \code{ds.funLevels} returns the levels prefixed by a fun message
#' @author Tim Cadman
#' @importFrom DSI datashield.aggregate
#' @examples
#' @export
ds.funLevels <- function(x=NULL, fun_message=NULL, datasources=NULL) {
Here we define a function. First we provide a name, which should be in camelCase prefixed by ds.
. We then provide the arguments for the function along with default values.
if(is.null(datasources)){
datasources <- datashield.connections_find()
}
This section checks whether a DataSHIELD connections object has been provided in the arguments. If not, it tries to find one in the local environment.
.check_args(x, fun_message)
This checks whether required inputs have been provided, and if they are of the correct class. The function .check_args
is defined at the end of the script.
cally <- call("funLevelsDS", x, fun_message)
This create a call object, which includes the name of the server-side function to be called and the arguments to be passed.
fun_levels <- DSI::datashield.aggregate(datasources, cally)
Using the DataSHIELD Interface (DSI) which handles API calls, we pass the call to the server-side. If the argument is valid and any server-side checks are passed, a value will be returned.
return(fun_levels)
The value that is returned from the server-side function is then returned to the user calling the client-side function.
#' Validate Arguments
#'
#' This function checks whether the provided arguments meet the expected conditions.
#' It ensures that `x` and `fun_message` are not `NULL` and that both are character vectors.
#'
#' @param x A character vector. Must not be `NULL`.
#' @param fun_message A character vector. Must not be `NULL`.
#'
#' @return This function does not return a value. It throws an error if any of the conditions are not met.
#'
#' @importFrom cli cli_abort
#'
#' @examples
#' # Valid input
#' .check_args("example", "This is a message")
#'
#' # Invalid input (NULL x)
#' # .check_args(NULL, "This is a message")
#'
#' # Invalid input (x not character)
#' # .check_args(123, "This is a message")
#'
.check_args <- function(x, fun_message) {
if(is.null(x)) {
cli_abort("Argument 'x' must not be NULL")
}
if(is.null(message)) {
cli_abort("Argument 'message' must not be NULL")
}
x_class <- class(x)
message_class <- class(fun_message)
if(x_class != "character") {
cli_abort(
c(
"x" = "`x` must be a character vector",
"i" = "You have provided an input with class {x_class}")
)
}
if(message_class != "character") {
cli_abort(
c(
"x" = "`fun_message` must be a character vector",
"i" = "You have provided an input with class {message_class}")
)
}
}
This function checks the input arguments and throws errors if no values are provided or they are of the wrong class. It uses cli_abort
to return errors rather than base R stop
. The CLI package enables the creation of more attractive, flexible error messages and we recommend using this for warning and error-handling in new DataSHIELD packages.
Rather than including all of the code in one block, by refactoring into separate sub-functions it is easier to read, test and debug. Here we define a separate function the purpose of which is to test the validity of the input arguments.