diff --git a/DESCRIPTION b/DESCRIPTION index 5e2aedf..68cf078 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: tidymodules Title: A robust framework for developing shiny modules -Version: 0.1.0.9003 +Version: 0.1.0.9004 Authors@R: c( person('Mustapha', 'Larbaoui', email = 'mustapha.larbaoui@novartis.com', role = c('cre', 'aut')), person('Douglas', 'Robinson', email = 'douglas.robinson@novartis.com', role = c('ctb')), @@ -19,7 +19,12 @@ Imports: digest, R6, visNetwork, - methods + methods, + snippr, + cli, + dplyr, + fs, + purrr RoxygenNote: 7.0.2 Roxygen: list(markdown = TRUE) URL: https://github.com/Novartis/tidymodules @@ -34,4 +39,6 @@ Suggests: RColorBrewer, shinyWidgets, plotly +Remotes: + dgrtwo/snippr@29c1813 VignetteBuilder: knitr diff --git a/NAMESPACE b/NAMESPACE index d9149ba..66bce75 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -428,9 +428,12 @@ export("%>>10%") export(ModStore) export(Store) export(TidyModule) +export(add_module) +export(add_tm_snippets) export(callModules) export(check_and_load) export(combine_ports) +export(defineEdges) export(getCacheOption) export(getMod) export(getSessionId) @@ -443,5 +446,22 @@ export(race_ports) export(session_type) export(showExamples) import(R6) +import(dplyr) import(shiny) +import(snippr) +importFrom(cli,cat_bullet) +importFrom(fs,dir_create) +importFrom(fs,dir_exists) +importFrom(fs,file_create) +importFrom(fs,file_exists) +importFrom(fs,path) +importFrom(fs,path_abs) +importFrom(fs,path_ext_set) +importFrom(fs,path_home_r) importFrom(methods,is) +importFrom(purrr,discard) +importFrom(purrr,keep) +importFrom(purrr,map) +importFrom(snippr,snippets_read) +importFrom(utils,file.edit) +importFrom(utils,menu) diff --git a/NEWS.md b/NEWS.md index a0a96ae..96d5c4c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,9 @@ +# tidymodules 0.1.0.9004 + +- add_module function +- snippets file & function to inject them into RStudio configuration +- new defineEdges() function for parsing module communication instructions + # tidymodules 0.1.0.9003 - Improve how the ports are moved around diff --git a/R/TidyModule.R b/R/TidyModule.R index ac92710..453e767 100644 --- a/R/TidyModule.R +++ b/R/TidyModule.R @@ -184,7 +184,7 @@ TidyModule <- R6::R6Class( isolate(x) }, #' @description - #' Function wrapper for port assignement expression. + #' Function wrapper for port assignment expression. #' @param x expression assignPort = function(x){ observe({ diff --git a/R/add_module.R b/R/add_module.R new file mode 100644 index 0000000..5938ab3 --- /dev/null +++ b/R/add_module.R @@ -0,0 +1,329 @@ +#' Create a module +#' +#' This function creates a `{tm}` module class inside the current folder. +#' +#' @param name The class name of the module. +#' @param path Where to created the file. Default is `getwd()`. The function will add `R` to the path if the sub-folder exists. +#' @param prefix filename prefix. Default is `tm`. Set to `NULL`` to disable. +#' @param inherit Parent module class. Default is TidyModule. +#' @param open Should the file be opened? +#' @param dir_create Creates the directory if it doesn't exist, default is `TRUE`. +#' @param export Logical. Should the module be exported? Default is `FALSE`. +#' @note As a convention, this function will automatically capitalize the first character of the `name` argument. +#' +#' @importFrom cli cat_bullet +#' @importFrom utils file.edit +#' @importFrom fs path_abs path file_create +#' @importFrom snippr snippets_read +#' +#' @export +add_module <- function( + name, + inherit = "TidyModule", + path = getwd(), + prefix = "tm", + open = TRUE, + dir_create = TRUE, + export = FALSE +){ + name <- file_path_sans_ext(name) + # Capitalize + name <- paste0(toupper(substring(name,1,1)),substring(name,2)) + + dir_created <- create_if_needed( + fs::path(path), type = "directory" + ) + if (!dir_created){ + cat_red_bullet( + "File not added (needs a valid directory)" + ) + return(invisible(FALSE)) + } + + if(dir.exists(fs::path(path, "R"))) + path <- fs::path(path, "R") + + old <- setwd(path_abs(path)) + on.exit(setwd(old)) + + where <- fs::path( + paste0(ifelse(is.null(prefix),"",paste0(prefix,"_")), name, ".R") + ) + + if(!check_file_exist(where)){ + cat_red_bullet( + "File not created (already exists)" + ) + return(invisible(FALSE)) + } + + # make sure the provided parent module is valid + import <- NULL + parent <- inherit + # TidyModule object + if(is(parent,"TidyModule")){ + parent <- class(parent)[1] + } + # Load the class generator from the name + if(class(parent) == "character") + tryCatch( + { + parent <- eval(parse(text = parent)) + }, + error = function(e){ + cat_red_bullet( + paste0("Could not find module defined with 'inherit' = ",inherit) + ) + return(invisible(FALSE)) + } + ) + # Retrieve package dependency and parent module name + if(is(parent,"R6ClassGenerator")){ + clist <- get_R6CG_list(parent) + if("TidyModule" %in% clist){ + import <- environmentName(parent$parent_env) + if(import == "R_GlobalEnv") + import <- NULL + parent <- clist[1] + }else{ + cat_red_bullet( + paste0("Could not find module defined with 'inherit' = ",deparse(substitute(inherit))) + ) + return(invisible(FALSE)) + } + } + + + # Retrieve content from package snippet + file_content <- snippr::snippets_read(path = system.file("rstudio/r.snippets",package = "tidymodules"))$tm.mod.new + file_content <- unlist(strsplit(file_content,"\\n")) + for(l in 1:length(file_content)){ + # remove $ escapes \\ + file_content[l] <- sub("\\$","$",file_content[l],fixed = TRUE) + # remove tabs + file_content[l] <- sub("\t","",file_content[l]) + # remove snippet placeholders + file_content[l] <- gsub("\\$\\{\\d+:(\\w+)\\}","%\\1",file_content[l]) + # remove cursor pointer + file_content[l] <- sub("\\$\\{0\\}","",file_content[l]) + # substitute module name + if(grepl("MyModule",file_content[l])){ + file_content[l] <- gsub("MyModule","s",file_content[l]) + file_content[l] <- sprintf(file_content[l],name) + } + # substitute parent module + if(grepl("TidyModule",file_content[l])){ + file_content[l] <- gsub("TidyModule","s",file_content[l]) + file_content[l] <- sprintf(file_content[l],parent) + } + # manage export + if (grepl("@export",file_content[l])){ + if(!export) + file_content[l] <- "#' @noRd " + if(!is.null(import)) + file_content[l] <- paste0("#'\n#' @import ",import,"\n",file_content[l]) + } + } + writeLines(file_content,where,sep = "\n") + + cat_created(fs::path(path,where)) + open_or_go_to(where, open) +} + +# bunch of utility functions copied from golem +# WILL FACILITATE MIGRATING THIS FUNCTION TO GOLEM + +#' @importFrom utils menu +yesno <- function (...) +{ + cat(paste0(..., collapse = "")) + menu(c("Yes", "No")) == 1 +} + +#' @importFrom fs file_exists +check_file_exist <- function(file){ + res <- TRUE + if (file_exists(file)){ + cat_orange_bullet(file) + res <- yesno("This file already exists, override?") + } + return(res) +} + +#' @importFrom fs dir_create file_create +create_if_needed <- function( + path, + type = c("file", "directory"), + content = NULL +){ + type <- match.arg(type) + # Check if file or dir already exist + if (type == "file"){ + dont_exist <- file_not_exist(path) + } else if (type == "directory"){ + dont_exist <- dir_not_exist(path) + } + # If it doesn't exist, ask if we are allowed + # to create it + if (dont_exist){ + ask <- yesno( + sprintf( + "The %s %s doesn't exist, create?", + basename(path), + type + ) + ) + # Return early if the user doesn't allow + if (!ask) { + return(FALSE) + } else { + # Create the file + if (type == "file"){ + if(dir_not_exist(dirname(path))) + dir_create(dirname(path), recurse = TRUE) + file_create(path) + write(content, path, append = TRUE) + } else if (type == "directory"){ + dir_create(path, recurse = TRUE) + } + } + } + + # TRUE means that file exists (either + # created or already there) + return(TRUE) +} + + +#' @importFrom cli cat_bullet +cat_green_tick <- function(...){ + cat_bullet( + ..., + bullet = "tick", + bullet_col = "green" + ) +} + +#' @importFrom cli cat_bullet +cat_red_bullet <- function(...){ + cat_bullet( + ..., + bullet = "bullet", + bullet_col = "red" + ) +} + +#' @importFrom cli cat_bullet +cat_orange_bullet <- function(...){ + cat_bullet( + ..., + bullet = "bullet", + bullet_col = "orange" + ) +} + +#' @importFrom cli cat_bullet +cat_info <- function(...){ + cat_bullet( + ..., + bullet = "arrow_right", + bullet_col = "grey" + ) +} + +cat_exists <- function(where){ + cat_red_bullet( + sprintf( + "%s already exists, skipping the copy.", + path_file(where) + ) + ) + cat_info( + sprintf( + "If you want replace it, remove the %s file first.", + path_file(where) + ) + ) +} + +cat_created <- function( + where, + file = "File" +){ + cat_green_tick( + sprintf( + "%s created at %s", + file, + where + ) + ) +} + +open_or_go_to <- function( + where, + open +){ + if ( + rstudioapi::isAvailable() && + open && + rstudioapi::hasFun("navigateToFile") + ){ + rstudioapi::navigateToFile(where) + } else { + cat_red_bullet( + sprintf( + "Go to %s", + where + ) + ) + } +} + +desc_exist <- function(pkg){ + file_exists( + paste0(pkg, "/DESCRIPTION") + ) +} + + +file_created_dance <- function( + where, + fun, + pkg, + dir, + name, + open +){ + cat_created(where) + + fun(pkg, dir, name) + + open_or_go_to(where, open) +} + +if_not_null <- function(x, ...){ + if (! is.null(x)){ + force(...) + } +} + +set_name <- function(x, y){ + names(x) <- y + x +} + +# FROM tools::file_path_sans_ext() & tools::file_ext +file_path_sans_ext <- function(x){ + sub("([^.]+)\\.[[:alnum:]]+$", "\\1", x) +} + +file_ext <- function (x) { + pos <- regexpr("\\.([[:alnum:]]+)$", x) + ifelse(pos > -1L, substring(x, pos + 1L), "") +} + +#' @importFrom fs dir_exists file_exists +dir_not_exist <- Negate(dir_exists) +file_not_exist <- Negate(file_exists) + + diff --git a/R/snippets.R b/R/snippets.R new file mode 100644 index 0000000..1675262 --- /dev/null +++ b/R/snippets.R @@ -0,0 +1,68 @@ +#' +#' @title Add `{tm}` snippets to RStudio +#' +#' @description This function adds useful `{tm}` code snippets to RStudio. +#' +#' @param force Force the re-installation when the snippets are already installed. +#' +#' @import snippr +#' @import dplyr +#' @importFrom fs path_home_r path_ext_set +#' @importFrom cli cat_bullet +#' @importFrom purrr keep discard map +#' @export +add_tm_snippets <- function(force = FALSE){ + # R snippets file + path <- path_home_r(".R", "snippets", path_ext_set("r","snippets")) + + if(!create_if_needed(path)) + cat_bullet("Skip installation of snippets", + bullet_col = "red", + bullet = "bullet") + + # retrieve current and new snippets + current_all_snippets <- snippets_get(path = path) + current_non_tm_snippets <- current_all_snippets %>% discard(grepl("^tm\\.",names(.),perl = TRUE)) + current_tm_snippets <- current_all_snippets %>% keep(grepl("^tm\\.",names(.),perl = TRUE)) + new_tm_snippets <- snippets_read(path = system.file("rstudio/r.snippets",package = "tidymodules")) + # calculate differences + del_snippets <- setdiff(names(current_tm_snippets),names(new_tm_snippets)) + keep_snippets <- intersect(names(current_tm_snippets),names(new_tm_snippets)) + add_snippets <- setdiff(names(new_tm_snippets),names(current_tm_snippets)) + # print some informations + if(length(del_snippets)>0){ + cat_bullet(paste0("Deleting ",length(del_snippets)," snippet(s):"), + bullet_col = "orange", + bullet = "bullet") + invisible(map(del_snippets,cat_bullet,bullet = "dot")) + } + existing_snippets <- current_non_tm_snippets + save_snippets <- NULL + if(length(keep_snippets) > 0) + if(force){ + cat_bullet(paste0("Re-installing ",length(keep_snippets)," existing snippet(s):"), + bullet_col = "green", + bullet = "tick") + invisible(map(keep_snippets,cat_bullet,bullet = "dot")) + save_snippets <- new_tm_snippets[keep_snippets] + }else{ + cat_bullet(paste0("Skip installation of ",length(keep_snippets)," existing snippet(s):"), + bullet_col = "red", + bullet = "bullet") + invisible(map(keep_snippets,cat_bullet,bullet = "dot")) + existing_snippets <- c(existing_snippets,current_tm_snippets[keep_snippets]) + } + if(length(add_snippets)>0){ + cat_bullet(paste0("Installing ",length(add_snippets)," new snippets:"), + bullet_col = "green", + bullet = "tick") + invisible(map(add_snippets,cat_bullet,bullet = "dot")) + save_snippets <- c(save_snippets,new_tm_snippets[add_snippets]) + } + + final_snippets <- existing_snippets + if(!is.null(save_snippets)) + final_snippets <- c(final_snippets,save_snippets) + + snippets_write(final_snippets,path = path) +} diff --git a/R/utility.R b/R/utility.R index bcac843..6040c7e 100644 --- a/R/utility.R +++ b/R/utility.R @@ -161,6 +161,21 @@ callModules <- function(){ }) lapply(calls,function(m) m$callModule()) } +#' +#' @title Function wrapper for ports connection expression. +#' +#' @description Used in server functions to define how modules are connected to each other. +#' +#' @param x expression +#' +#' @export +defineEdges <- function(x){ + observe({ + isolate(x) + }) +} + + #' #' @title Retrieve cache option from the environment #' @@ -268,3 +283,22 @@ getSessionId <- function(session = getDefaultReactiveDomain()){ return(sid) } } + + +#' +#' @title Recursive function for retrieving R6ClassGenerator inheritance +#' +#' @description This function is used to retrieve a list of class name that a R6ClassGenerator object inherit from. +#' +#' @param r6cg A R6ClassGenerator object. +#' +#' @return vector of class names +get_R6CG_list <- function(r6cg){ + if(!is(r6cg,"R6ClassGenerator")) + stop("provide a R6ClassGenerator object!") + clist <- r6cg$classname + if(!is.null(r6cg$get_inherit())) + clist <- c(clist,get_R6CG_list(r6cg$get_inherit())) + + return(clist) +} diff --git a/inst/rstudio/r.snippets b/inst/rstudio/r.snippets new file mode 100644 index 0000000..958e25a --- /dev/null +++ b/inst/rstudio/r.snippets @@ -0,0 +1,73 @@ +snippet tm.mod.new + #' + #' ${1:MyModule} Module. + #' + #' @description + #' This \href{https://opensource.nibr.com/tidymodules}{`{tm}`} module is a R6 class representing a ${1:MyModule}. + #' + #' @family tm + #' + #' @details + #' More details about your module here. + #' + #' @export + ${1:MyModule} <- R6::R6Class( + classname = "${1:MyModule}", + inherit = ${2:TidyModule}, + public = list( + #' @description + #' Module's initialization function. + #' @param ... options + #' @return An instance of ${1:MyModule} + initialize = function(...){ + # Don't remove the line below + super\$initialize(...) + + # Ports definition starts here... + ${0} + }, + #' @description + #' Module's ui function. + #' @return HTML tags list. + ui = function(){ + # Module's representation starts here ... + tagList() + }, + #' @description + #' Module's server function. + #' @param input Shiny input + #' @param output Shiny output + #' @param session Shiny session + server = function(input, output, session){ + # Don't remove the line below + super\$server(input,output,session) + + # Module server logic starts here ... + + } + ) + ) +snippet tm.port.define + self\$definePort({ + ${0} + }) +snippet tm.port.in + self\$addInputPort( + name = "${1:port_name}", + description = "A clear description for this input port${0}", + sample = data.frame(x = rnorm(10), y = rnorm(10)) + ) +snippet tm.port.out + self\$addOutputPort( + name = "${1:port_name}", + description = "A clear description for this output port${0}", + sample = data.frame(x = rnorm(10), y = rnorm(10)) + ) +snippet tm.port.assign + self\$assignPort({ + ${0} + }) +snippet tm.port.edges + defineEdges({ + ${0} + }) diff --git a/inst/shiny/examples/4_communication/app.R b/inst/shiny/examples/4_communication/app.R index ea0c3fb..e506aa3 100644 --- a/inst/shiny/examples/4_communication/app.R +++ b/inst/shiny/examples/4_communication/app.R @@ -61,16 +61,13 @@ server <- function(input, output, session) { # Add modules server logic callModules() # Configure modules communication by connecting ports - observe({ + defineEdges({ # dataset selector provides data to # column mapper and row filter modules - # getMod("Marzie") %1>1% getMod("Renan") - browser() oport("Marzie","dataset") %->>% - iport("Renan","data") %->% - iport("Stefan","data") + iport("Renan","data") %->% + iport("Stefan","data") - mod("Marzie") %1>1% mod("Stefan") # the mappings are then used by the plot generator mod("Renan") %1>1% mod("Doug") # plot generator also takes raw and filtered data as input diff --git a/man/TidyModule.Rd b/man/TidyModule.Rd index de1ba3e..9f88ca9 100644 --- a/man/TidyModule.Rd +++ b/man/TidyModule.Rd @@ -224,7 +224,7 @@ Function wrapper for port definition expression. \if{html}{\out{