From e3e98bcbfe27f8694172c7049bdd6809836ef0c9 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Fri, 9 Jan 2026 09:42:18 +0100 Subject: [PATCH 1/6] refactor colnames --- R/colnamesDS.R | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 R/colnamesDS.R diff --git a/R/colnamesDS.R b/R/colnamesDS.R new file mode 100644 index 00000000..6dc2e99e --- /dev/null +++ b/R/colnamesDS.R @@ -0,0 +1,17 @@ +#' +#' @title Returns the column names of a data frame or matrix +#' @description This function is similar to R function \code{colnames}. +#' @details The function returns the column names of the input dataframe or matrix +#' @param x a string character, the name of a dataframe or matrix +#' @return the column names of the input object +#' @author Demetris Avraam, for DataSHIELD Development Team +#' @export +#' +colnamesDS <- function(x){ + x.val <- .loadServersideObject(x) + .checkClass(obj = x.val, obj_name = x, permitted_classes = c("data.frame", "matrix")) + out <- colnames(x.val) + return(out) +} +#AGGREGATE FUNCTION +# colnamesDS From c130e19f8e466a50365dbf94baf0bd4044997ed7 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Fri, 9 Jan 2026 09:44:19 +0100 Subject: [PATCH 2/6] added reusable functions --- R/utils.R | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 R/utils.R diff --git a/R/utils.R b/R/utils.R new file mode 100644 index 00000000..6fd53936 --- /dev/null +++ b/R/utils.R @@ -0,0 +1,42 @@ +#' Load a Server-Side Object by Name +#' +#' Evaluates a character string referring to an object name and returns the corresponding +#' object from the parent environment. If the object does not exist, an error is raised. +#' +#' @param x A character string naming the object to be retrieved. +#' @return The evaluated R object referred to by `x`. +#' @noRd +.loadServersideObject <- function(x) { + tryCatch( + eval(parse(text = x), envir = parent.frame(2)), + error = function(e) { + stop("The server-side object", " '", x, "' ", "does not exist") + } + ) +} + +#' Check Class of a Server-Side Object +#' +#' Verifies that a given object is of an allowed class. If not, raises an informative error +#' message listing the permitted classes and the actual class of the object. +#' +#' @param obj The object whose class should be checked. +#' @param obj_name A character string with the name of the object (used in error messages). +#' @param permitted_classes A character vector of allowed class names. +#' @importFrom glue glue glue_collapse +#' @return Invisibly returns `TRUE` if the class check passes; otherwise throws an error. +#' @noRd +.checkClass <- function(obj, obj_name, permitted_classes) { + typ <- class(obj) + + if (!any(permitted_classes %in% typ)) { + msg <- glue( + "The server-side object must be of type {glue_collapse(permitted_classes, sep = ' or ')}. ", + "'{obj_name}' is type {typ}." + ) + + stop(msg, call. = FALSE) + } + + invisible(TRUE) +} From 490c1f89df29dafadf8ba1c32ced590fd72b53b6 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Fri, 9 Jan 2026 09:45:16 +0100 Subject: [PATCH 3/6] added tests --- tests/testthat/test-smk-colnamesDS.R | 67 ++++++++++++++++++++++++++++ tests/testthat/test-smk-utils.R | 53 ++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 tests/testthat/test-smk-colnamesDS.R create mode 100644 tests/testthat/test-smk-utils.R diff --git a/tests/testthat/test-smk-colnamesDS.R b/tests/testthat/test-smk-colnamesDS.R new file mode 100644 index 00000000..36c4ceef --- /dev/null +++ b/tests/testthat/test-smk-colnamesDS.R @@ -0,0 +1,67 @@ +#------------------------------------------------------------------------------- +# Copyright (c) 2019-2022 University of Newcastle upon Tyne. All rights reserved. +# Copyright (c) 2022-2025 Arjuna Technologies, Newcastle upon Tyne. All rights reserved. +# +# This program and the accompanying materials +# are made available under the terms of the GNU Public License v3.0. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +#------------------------------------------------------------------------------- + +# +# Set up +# + +# context("colnamesDS::smk::setup") + +# +# Tests +# + +# context("colnamesDS::smk::data.frame") +test_that("simple colnamesDS, data.frame", { + input <- data.frame(v1 = c(0.0, 1.0, 2.0, 3.0, 4.0), v2 = c(4.0, 3.0, 2.0, 1.0, 0.0)) + + res <- colnamesDS("input") + + expect_equal(class(res), "character") + expect_length(res, 2) + expect_true("v1" %in% res) + expect_true("v2" %in% res) +}) + +# context("colnamesDS::smk::data.matrix") +test_that("simple colnamesDS, data.matrix", { + input <- data.matrix(data.frame(v1 = c(0.0, 1.0, 2.0, 3.0, 4.0), v2 = c(4.0, 3.0, 2.0, 1.0, 0.0))) + + res <- colnamesDS("input") + + expect_equal(class(res), "character") + expect_length(res, 2) + expect_true("v1" %in% res) + expect_true("v2" %in% res) +}) + +test_that("colnamesDS throws error when object does not exist", { + expect_error( + colnamesDS("nonexistent_object"), + regexp = "does not exist" + ) +}) + +test_that("colnamesDS throws error when object is not data.frame or matrix", { + bad_input <- list(a = 1:3, b = 4:6) + expect_error( + colnamesDS("bad_input"), + regexp = "must be of type data.frame or matrix" + ) +}) + +# +# Done +# + +# context("colnamesDS::smk::shutdown") + +# context("colnamesDS::smk::done") \ No newline at end of file diff --git a/tests/testthat/test-smk-utils.R b/tests/testthat/test-smk-utils.R new file mode 100644 index 00000000..2c733cfb --- /dev/null +++ b/tests/testthat/test-smk-utils.R @@ -0,0 +1,53 @@ + +#------------------------------------------------------------------------------- +# Copyright (c) 2019-2022 University of Newcastle upon Tyne. All rights reserved. +# +# This program and the accompanying materials +# are made available under the terms of the GNU Public License v3.0. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +#------------------------------------------------------------------------------- + +# +# Set up +# + +## When .loadServersideObject is called, the actual data exists two levels below the function, +## i.e. data in global env --> ds function --> .loadServersideObject. We recreate this in +## the test environment with a wrapper function. +.dsFunctionWrapper <- function(x) { + .loadServersideObject(x) +} + +# context("utils::smk::setup") +test_that(".loadServersideObject() returns existing object", { + test_df <- data.frame(a = 1:3) + result <- .dsFunctionWrapper("test_df") + expect_identical(result, test_df) +}) + +test_that(".loadServersideObject() throws error for missing object", { + expect_error( + .dsFunctionWrapper("test_df"), + regexp = "does not exist" + ) +}) + +test_that(".checkClass() passes for correct class", { + df <- data.frame(a = 1) + expect_invisible( + .checkClass(df, "df", c("data.frame", "matrix")) + ) +}) + +test_that(".checkClass() throws informative error for wrong class", { + x <- list(a = 1) + expect_error( + .checkClass(x, "x", c("data.frame", "matrix")), + regexp = "must be of type data.frame or matrix" + ) +}) + +# context("utils::smk::shutdown") +# context("utils::smk::done") From 4305357be60acb412aed9b03e938c555bae76765 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Fri, 9 Jan 2026 09:52:21 +0100 Subject: [PATCH 4/6] redocumented package --- NAMESPACE | 3 +++ man/colnamesDS.Rd | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 man/colnamesDS.Rd diff --git a/NAMESPACE b/NAMESPACE index c8a715fb..db4a5378 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -25,6 +25,7 @@ export(changeRefGroupDS) export(checkNegValueDS) export(checkPermissivePrivacyControlLevel) export(classDS) +export(colnamesDS) export(completeCasesDS) export(corDS) export(corTestDS) @@ -139,3 +140,5 @@ import(gamlss.dist) import(mice) importFrom(gamlss.dist,pST3) importFrom(gamlss.dist,qST3) +importFrom(glue,glue) +importFrom(glue,glue_collapse) diff --git a/man/colnamesDS.Rd b/man/colnamesDS.Rd new file mode 100644 index 00000000..e13abde4 --- /dev/null +++ b/man/colnamesDS.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/colnamesDS.R +\name{colnamesDS} +\alias{colnamesDS} +\title{Returns the column names of a data frame or matrix} +\usage{ +colnamesDS(x) +} +\arguments{ +\item{x}{a string character, the name of a dataframe or matrix} +} +\value{ +the column names of the input object +} +\description{ +This function is similar to R function \code{colnames}. +} +\details{ +The function returns the column names of the input dataframe or matrix +} +\author{ +Demetris Avraam, for DataSHIELD Development Team +} From 57de79ff97fb95e4240f9d55a65a7d481454d29a Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Fri, 9 Jan 2026 10:20:10 +0100 Subject: [PATCH 5/6] added PR template --- PULL_REQUEST_TEMPLATE.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 PULL_REQUEST_TEMPLATE.md diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..1cbc8c4a --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,20 @@ +## Instructions & checklist for PR author + +### Description of changes +[Add descriptions of changes made] + +### Refactor instructions +- [ ] Replaced `x <- eval(parse(text = x.name), envir = parent.frame())` with `x <- .loadServersideObject(x)` +- [ ] If necessary, check the class of the object using `.checkClass()` + +### Testing instructions +- [ ] Writen server-side unit tests for unhappy flow +- [ ] Run `devtools::test(filter = "smk-|disc|arg")` and check it passes +- [ ] Run `devtools::check(args = '--no-tests')` and check it passes (we run tests separately to skip performance checks) +- [ ] Run `devtools::build()` and check it builds without errors + +## Instructions & checklist for PR reviewers +- [ ] Run `devtools::test(filter = "smk-|disc|arg")` and check it passes +- [ ] Run `devtools::check(args = '--no-tests')` and check it passes (we run tests separately to skip performance checks) +- [ ] Run `devtools::build()` and check it builds without errors + From 40eb96a8bbec964a2a3af83fb5825dd561b2c3fc Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Fri, 9 Jan 2026 11:30:05 +0100 Subject: [PATCH 6/6] use get instead of eval as more secure --- R/utils.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/utils.R b/R/utils.R index 6fd53936..56e24928 100644 --- a/R/utils.R +++ b/R/utils.R @@ -8,7 +8,7 @@ #' @noRd .loadServersideObject <- function(x) { tryCatch( - eval(parse(text = x), envir = parent.frame(2)), + get(x, envir = parent.frame(2)), error = function(e) { stop("The server-side object", " '", x, "' ", "does not exist") }