Skip to content

Commit a79e87a

Browse files
authored
Merge pull request #451 from datashield/v7.0-dev-feat-colnames
V7.0 dev feat colnames
2 parents 6eaba77 + 6857bf4 commit a79e87a

8 files changed

Lines changed: 241 additions & 0 deletions

File tree

NAMESPACE

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export(changeRefGroupDS)
2525
export(checkNegValueDS)
2626
export(checkPermissivePrivacyControlLevel)
2727
export(classDS)
28+
export(colnamesDS)
2829
export(completeCasesDS)
2930
export(corDS)
3031
export(corTestDS)
@@ -139,3 +140,5 @@ import(gamlss.dist)
139140
import(mice)
140141
importFrom(gamlss.dist,pST3)
141142
importFrom(gamlss.dist,qST3)
143+
importFrom(glue,glue)
144+
importFrom(glue,glue_collapse)

PULL_REQUEST_TEMPLATE.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
## Instructions & checklist for PR author
2+
3+
### Description of changes
4+
[Add descriptions of changes made]
5+
6+
### Refactor instructions
7+
- [ ] Replaced `x <- eval(parse(text = x.name), envir = parent.frame())` with `x <- .loadServersideObject(x)`
8+
- [ ] If necessary, check the class of the object using `.checkClass()`
9+
10+
### Testing instructions
11+
- [ ] Writen server-side unit tests for unhappy flow
12+
- [ ] Run `devtools::test(filter = "smk-|disc|arg")` and check it passes
13+
- [ ] Run `devtools::check(args = '--no-tests')` and check it passes (we run tests separately to skip performance checks)
14+
- [ ] Run `devtools::build()` and check it builds without errors
15+
16+
## Instructions & checklist for PR reviewers
17+
- [ ] Run `devtools::test(filter = "smk-|disc|arg")` and check it passes
18+
- [ ] Run `devtools::check(args = '--no-tests')` and check it passes (we run tests separately to skip performance checks)
19+
- [ ] Run `devtools::build()` and check it builds without errors
20+

R/colnamesDS.R

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#'
2+
#' @title Returns the column names of a data frame or matrix
3+
#' @description This function is similar to R function \code{colnames}.
4+
#' @details The function returns the column names of the input dataframe or matrix
5+
#' @param x a string character, the name of a dataframe or matrix
6+
#' @return the column names of the input object
7+
#' @author Demetris Avraam, for DataSHIELD Development Team
8+
#' @export
9+
#'
10+
colnamesDS <- function(x){
11+
x.val <- .loadServersideObject(x)
12+
.checkClass(obj = x.val, obj_name = x, permitted_classes = c("data.frame", "matrix"))
13+
out <- colnames(x.val)
14+
return(out)
15+
}
16+
#AGGREGATE FUNCTION
17+
# colnamesDS

R/utils.R

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#' Load a Server-Side Object by Name
2+
#'
3+
#' Evaluates a character string referring to an object name and returns the corresponding
4+
#' object from the parent environment. If the object does not exist, an error is raised.
5+
#'
6+
#' @param x A character string naming the object to be retrieved.
7+
#' @return The evaluated R object referred to by `x`.
8+
#' @noRd
9+
.loadServersideObject <- function(x) {
10+
tryCatch(
11+
get(x, envir = parent.frame(2)),
12+
error = function(e) {
13+
stop("The server-side object", " '", x, "' ", "does not exist")
14+
}
15+
)
16+
}
17+
18+
#' Check Class of a Server-Side Object
19+
#'
20+
#' Verifies that a given object is of an allowed class. If not, raises an informative error
21+
#' message listing the permitted classes and the actual class of the object.
22+
#'
23+
#' @param obj The object whose class should be checked.
24+
#' @param obj_name A character string with the name of the object (used in error messages).
25+
#' @param permitted_classes A character vector of allowed class names.
26+
#' @importFrom glue glue glue_collapse
27+
#' @return Invisibly returns `TRUE` if the class check passes; otherwise throws an error.
28+
#' @noRd
29+
.checkClass <- function(obj, obj_name, permitted_classes) {
30+
typ <- class(obj)
31+
32+
if (!any(permitted_classes %in% typ)) {
33+
msg <- glue(
34+
"The server-side object must be of type {glue_collapse(permitted_classes, sep = ', ', last = ' or ')}. ",
35+
"'{obj_name}' is type {glue_collapse(typ, sep = ', ', last = ' and ')}."
36+
)
37+
38+
stop(msg, call. = FALSE)
39+
}
40+
41+
invisible(TRUE)
42+
}

man/colnamesDS.Rd

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#-------------------------------------------------------------------------------
2+
# Copyright (c) 2019-2022 University of Newcastle upon Tyne. All rights reserved.
3+
# Copyright (c) 2022-2025 Arjuna Technologies, Newcastle upon Tyne. All rights reserved.
4+
#
5+
# This program and the accompanying materials
6+
# are made available under the terms of the GNU Public License v3.0.
7+
#
8+
# You should have received a copy of the GNU General Public License
9+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
10+
#-------------------------------------------------------------------------------
11+
12+
#
13+
# Set up
14+
#
15+
16+
# context("colnamesDS::smk::setup")
17+
18+
#
19+
# Tests
20+
#
21+
22+
# context("colnamesDS::smk::data.frame")
23+
test_that("simple colnamesDS, data.frame", {
24+
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))
25+
26+
res <- colnamesDS("input")
27+
28+
expect_equal(class(res), "character")
29+
expect_length(res, 2)
30+
expect_true("v1" %in% res)
31+
expect_true("v2" %in% res)
32+
})
33+
34+
# context("colnamesDS::smk::data.matrix")
35+
test_that("simple colnamesDS, data.matrix", {
36+
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)))
37+
38+
res <- colnamesDS("input")
39+
40+
expect_equal(class(res), "character")
41+
expect_length(res, 2)
42+
expect_true("v1" %in% res)
43+
expect_true("v2" %in% res)
44+
})
45+
46+
test_that("colnamesDS throws error when object does not exist", {
47+
expect_error(
48+
colnamesDS("nonexistent_object"),
49+
regexp = "does not exist"
50+
)
51+
})
52+
53+
test_that("colnamesDS throws error when object is not data.frame or matrix", {
54+
bad_input <- list(a = 1:3, b = 4:6)
55+
expect_error(
56+
colnamesDS("bad_input"),
57+
regexp = "must be of type data.frame or matrix"
58+
)
59+
})
60+
61+
#
62+
# Done
63+
#
64+
65+
# context("colnamesDS::smk::shutdown")
66+
67+
# context("colnamesDS::smk::done")

tests/testthat/test-smk-utils.R

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
2+
#-------------------------------------------------------------------------------
3+
# Copyright (c) 2019-2022 University of Newcastle upon Tyne. All rights reserved.
4+
#
5+
# This program and the accompanying materials
6+
# are made available under the terms of the GNU Public License v3.0.
7+
#
8+
# You should have received a copy of the GNU General Public License
9+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
10+
#-------------------------------------------------------------------------------
11+
12+
#
13+
# Set up
14+
#
15+
16+
## When .loadServersideObject is called, the actual data exists two levels below the function,
17+
## i.e. data in global env --> ds function --> .loadServersideObject. We recreate this in
18+
## the test environment with a wrapper function.
19+
.dsFunctionWrapper <- function(x) {
20+
.loadServersideObject(x)
21+
}
22+
23+
# context("utils::smk::setup")
24+
test_that(".loadServersideObject() returns existing object", {
25+
test_df <- data.frame(a = 1:3)
26+
result <- .dsFunctionWrapper("test_df")
27+
expect_identical(result, test_df)
28+
})
29+
30+
test_that(".loadServersideObject() throws error for missing object", {
31+
expect_error(
32+
.dsFunctionWrapper("test_df"),
33+
regexp = "does not exist"
34+
)
35+
})
36+
37+
test_that(".checkClass() passes for correct class", {
38+
df <- data.frame(a = 1)
39+
expect_invisible(
40+
.checkClass(df, "df", c("data.frame", "matrix"))
41+
)
42+
})
43+
44+
test_that(".checkClass() throws informative error for wrong class with one target class", {
45+
x <- list(a = 1)
46+
expect_error(
47+
.checkClass(x, "x", "data.frame"),
48+
regexp = "The server-side object must be of type data.frame. 'x' is type list."
49+
)
50+
})
51+
52+
test_that(".checkClass() throws informative error for wrong class with three target classes", {
53+
x <- list(a = 1)
54+
expect_error(
55+
.checkClass(x, "x", c("data.frame", "matrix", "unicorn")),
56+
regexp = "The server-side object must be of type data.frame, matrix or unicorn. 'x' is type list."
57+
)
58+
})
59+
60+
test_that(".checkClass() throws informative error for three target classes and three actual classes", {
61+
x <- tibble(a = 1)
62+
expect_error(
63+
.checkClass(x, "x", c("Boolean", "unicorn", "donkey")),
64+
regexp = "The server-side object must be of type Boolean, unicorn or donkey. 'x' is type tbl_df, tbl and data.frame."
65+
)
66+
})
67+
68+
# context("utils::smk::shutdown")
69+
# context("utils::smk::done")

0 commit comments

Comments
 (0)