Skip to content

Commit

Permalink
tidyup code and docs
Browse files Browse the repository at this point in the history
  • Loading branch information
gogonzo committed Oct 31, 2023
1 parent 3eaf0b6 commit e421874
Show file tree
Hide file tree
Showing 12 changed files with 395 additions and 273 deletions.
4 changes: 3 additions & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ LazyData: true
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.2.3
Collate:
'ddl.R'
'ddl-class.R'
'ddl-modules.R'
'ddl-run.R'
'dummy_functions.R'
'get_rcode_utils.R'
'include_css_js.R'
Expand Down
143 changes: 143 additions & 0 deletions R/ddl-class.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
#' DDL object
#'
#' Object to execute custom DDL code in the shiny session.
#'
#' @section Creating reproducible data:
#' `ddl` object can be used to create reproducible data in the shiny session. `ddl$server` function
#' can execute any R code and return [`teal.data::teal_data-class`]. For reproducibility purposes,
#' we recommend to initialize empty `teal_data` object and evaluate necessary code with `eval_code` or `within`.
#' ```r
#' function(id, ...) {
#' moduleServer(id, function(input, output, session) {
#' eventReactive(input$submit, {
#' data <- teal_data() |> within({
#' # code to be run when app user presses submit
#' })
#' })
#' })
#' }
#' ```
#' Obtained data is passed further in the `teal` app with `code` which can be used to recreate the objects.
#'
#' @section Code masking:
#' `ddl` object can be used in a way that evaluated code is different than the code
#' returned in `teal_data` object. Typically occurs when app user is asked to input a
#' password and we'd like to skip this input in the reproducible code. Possibly, users password
#' could be substituted with `askpass::askpass()` call, so the returned code is still executable but secure.
#' `ddl` developer must understand that this is a security risk and should be handled with care.
#' To make sure that the code is reproducible, `ddl` object should be used with `input_mask` argument.
#' `teal` provides convenience function [ddl_run()] which handles evaluation of the code, masking
#' and creating `teal_data` object. Such `server` function could look like this:
#'
#' ```
#' server = function(id, ...) {
#' moduleServer(id, function(input, output, session) {
#' reactive({
#' ddl_run(input = input, ...)
#' })
#' })
#' }
#' ```
#'
#' If `ddl` developer values more control, then might be interested in using `...` explicitly,
#' and create `teal_data` object manually.
#'
#' @param ui (`shiny.tag`)\cr
#' `shiny` user-interface module containing inputs whose `id` correspond to the arguments in the `code`.
#'
#' @param server (`function`)\cr
#' `shiny` server module [`teal.data::teal_data-class`] possibly wrapped in a [reactive()].
#' `server` function should have `id` and `...` as formals. Where:
#' - `id` is a `shiny` module id, and
#' - `...` passes arguments from the `ddl` object (`code`, `input_mask`, `datanames`, `join_keys`).
#' See section `Code masking`.
#'
#' @param expr (optional `expression`)\cr
#' Syntactically valid R expression to be executed in the shiny session.
#' Shouldn't be specified when `code` is specified.
#'
#' @param code (optional `character` or `language`)\cr
#' Object containing (defused) syntactically valid R expression to be executed in the shiny session.
#' Shouldn't be specified when `expr` is specified.
#'
#' @param input_mask (optional `named list`)\cr
#' arguments to be substituted in the `code`. These (named) list elements are going to replace
#' symbols in the code prefixed with `input$` or `input[["`. Typically `input_mask` is used
#' to mask username or password with `list(password = quote(askpass::askpass()))`.
#' See section `code masking` for more details.
#'
#' @param datanames (optional `character`)\cr
#' Names of the datasets created by evaluation of the `code`. By default, `datanames`
#' are obtained from the `join_keys` or from results of the `code` evaluation.
#' If `code` evaluation creates objects which are not considered as datasets, they
#' should be omitted from `datanames` to avoid errors.
#'
#' @inheritParams teal.data::teal_data
#'
#' @export
ddl <- function(expr,
code,
input_mask = list(),
ui = submit_button_ui,
server = submit_button_server,
join_keys = teal.data::join_keys(),
datanames = names(join_keys$get())) {
checkmate::assert_list(input_mask)
checkmate::check_function(ui, args = "id")
checkmate::check_function(server, args = c("id", "..."))
checkmate::check_class(join_keys, "JoinKeys")
checkmate::check_character(datanames, min.len = 1)

out <- structure(
list(ui = ui, server = server),
input_mask = input_mask,
datanames = datanames,
join_keys = join_keys,
class = "ddl"
)

if (!missing(expr) || !missing(code)) {
# this is intended to be used with input mask
# but in the same time we can't forbid user to use it
# without input_mask. Some users might prefer to use ddl_run
# to automaticaly handle their code.
# Q: can NEST bear responsibility for reproducibility of the masked code?
if (!missing(expr)) {
code <- substitute(expr)
}
if (is.character(code)) {
code <- parse(text = code)
}
attr(out, "code") <- code
}

out
}

# methods from teal.data ----
# to be removed soon

#' Get data names from `ddl`
#' @rdname get_dataname
#' @param x (`ddl`) object
#' @export
get_dataname.ddl <- function(x) {
attr(x, "datanames")
}

#' @rdname get_join_keys
#' @export
get_join_keys.ddl <- function(data) {
attr(data, "join_keys")
}

# todo: to remove before merge -------------
#' @export
open_conn <- function(username, password) {
if (password != "pass") stop("Invalid credentials. 'pass' is the password") else TRUE
}
#' @export
close_conn <- function(conn) {
message("closed")
return(NULL)
}
59 changes: 59 additions & 0 deletions R/ddl-modules.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#' Run code and mask inputs
#'
#' Delayed Data Loading module with login and password input.
#'
#' @name submit_button_module
#'
#'
#' @param id (`character`) `shiny` module id.
#' @param ... (`list`) arguments passed to `ddl_run` function.
#' @return `shiny` module
NULL

#' @rdname submit_button_module
#' @export
submit_button_ui <- function(id) {
ns <- NS(id)
actionButton(inputId = ns("submit"), label = "Submit")
}

#' @rdname submit_button_module
#' @export
submit_button_server <- function(id, ...) {
moduleServer(id, function(input, output, session) {
tdata <- eventReactive(input$submit, {
ddl_run(input = input, ...)
})

# would need to make sure we handle reactivity correctly here as teal::init expects not reactive teal_data...
return(tdata)
})
}

#' Wrapper for `ui` and `server` in `ddl` object
#'
#' Convenience wrapper for `ui` and `server` functions in `ddl` object.
#' On the `server` side, function calls `shiny` module and adjusts the arguments
#' to the formals of the `server` function.
#' @param id (`character`) `shiny` module id.
#' @param x (`ddl`) object.
#' @name ddl_module
#' @return `shiny` module
#' @keywords internal
NULL

#' @rdname ddl_module
#' @keywords internal
ddl_server <- function(id, x) {
# subset attributes to only those that are arguments of the server function
args <- names(formals(x$server))
attrs <- attributes(x)
attrs <- attrs[setdiff(names(attrs), c("id", "class", "names"))]
do.call(x$server, c(list(id = id), attrs))
}

#' @rdname ddl_module
#' @keywords internal
ddl_ui <- function(id, x) {
x$ui(id = id)
}
77 changes: 77 additions & 0 deletions R/ddl-run.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#' Run code and mask inputs
#'
#' Function runs the `code`, masks the `code` and creates `teal_data` object.
#' @param input (`list`) containing inputs to be used in the `code`
#' @param code (`language`) code to be executed
#' @param input_mask (`list`) containing inputs to be masked in the `code`
#' @param datanames (`character`) names of the objects to be created from the code evaluation
#' @param join_keys (`join_keys`) object
#'
#' @return `teal_data` object
#'
#' @export
ddl_run <- function(input = list(),
code,
input_mask = list(),
join_keys = teal.data::join_keys(),
datanames = names(join_keys$get())) {
checkmate::assert_list(input)
if (inherits(input, "reactivevalues")) {
input <- shiny::reactiveValuesToList(input)
}
data <- teal_data(join_keys = join_keys)

# evaluate code and substitute input
data <- teal.code::eval_code(data, .substitute_inputs(code, args = input))

if (identical(ls(data@env), character(0))) {
warning(
"Evaluation of `ddl` code haven't created any objects.\n",
"Please make sure that the code is syntactically correct and creates necessary data."
)
}

if (!missing(input_mask)) {
# mask dynamic inputs with mask
input <- utils::modifyList(input, input_mask)

# replace code of teal_data with masked code
# question: warnings and errors are not masked, is it ok?
data@code <- format_expression(.substitute_inputs(code, args = input))
}

if (length(datanames)) {
datanames(data) <- datanames
}
if (length(datanames(data)) == 0) {
datanames(data) <- ls(data@env)
}

data
}

#' substitute inputs in the code
#'
#' Function replaces symbols in the provided code prefixed with `input$` or `input[["`
#' by values of the `args` argument.
#'
#' @param code (`language`) code to substitute
#' @param args (`list`) named list or arguments
.substitute_inputs <- function(code, args) {
code <- if (identical(as.list(code)[[1L]], as.symbol("{"))) {
as.list(code)[-1L]
} else {
code
}

code_strings <- vapply(code, deparse1, character(1L))
code_strings <- gsub("(input\\$)(\\w+)", "\\.(\\2\\)", code_strings)
code_strings <- gsub("(input\\[\\[\")(\\w+)(\"\\]\\])", "\\.(\\2\\)", code_strings)

# Use bquote to obtain code with input values and masking values.
as.expression(
lapply(code_strings, function(x) {
do.call(bquote, list(str2lang(x), list2env(args)))
})
)
}
Loading

0 comments on commit e421874

Please sign in to comment.