From d046f29c1aae38d8103d098c21ea9326729a99a2 Mon Sep 17 00:00:00 2001 From: John Coene Date: Sat, 9 Mar 2024 21:49:57 +0100 Subject: [PATCH] feat: error handling --- DESCRIPTION | 4 ++-- NAMESPACE | 1 + R/aaa.R | 1 + R/core.R | 40 +++++++++++++++++++++++++++++++++++++++- README.md | 2 ++ docs/_coverpage.md | 2 +- docs/index.html | 5 ++++- docs/js.md | 2 ++ docs/r.md | 43 +++++++++++++++++++++++++++++++++++++++++++ man/constructors.Rd | 3 +++ srcjs/index.ts | 4 ++-- 11 files changed, 100 insertions(+), 7 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 9b91f69..519d833 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: communicate Title: Communicate Between 'Shiny' Client and Server -Version: 0.1.2.9001 +Version: 0.1.3.9000 Authors@R: c( person(given = "John", @@ -15,7 +15,7 @@ Description: What the package does (one paragraph). License: GPL (>= 3) Encoding: UTF-8 Roxygen: list(markdown = TRUE) -RoxygenNote: 7.2.3 +RoxygenNote: 7.3.0 Imports: shiny, jsonlite, diff --git a/NAMESPACE b/NAMESPACE index 8af5b43..e64ce9d 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -2,6 +2,7 @@ S3method(print,converters_fn) S3method(print,defaults_fn) +S3method(print,errors_fn) export(Character) export(Dataframe) export(Date) diff --git a/R/aaa.R b/R/aaa.R index e7756c8..e82d278 100644 --- a/R/aaa.R +++ b/R/aaa.R @@ -6,3 +6,4 @@ env$prefix <- NULL env$handlers <- list() env$schemas <- list() env$defaults <- list() +env$errors <- list() diff --git a/R/core.R b/R/core.R index 231ac1e..aa70e01 100644 --- a/R/core.R +++ b/R/core.R @@ -47,7 +47,23 @@ com <- \(id, handler) { # defaults may be reactives check_args_match(env$handlers[[id]], ...) env$defaults[[id]] <- list(...) - com_send(id) + + on.exit( + com_send(id), + add = TRUE + ) + + fn <- \(handler, ...){ + if(!is.function(handler)) + stop("Handler must be a function") + + if(length(methods::formalArgs(handler)) < 1L) + stop("Handler must have at least one argument") + + env$errors[[id]] <- handler + } |> + construct_errors_fn() |> + invisible() } fn |> @@ -82,6 +98,15 @@ construct_converters_fn <- \(fn) { ) } +#' @rdname constructors +#' @keywords internal +construct_errors_fn <- \(fn) { + structure( + fn, + class = c("errors_fn", class(fn)) + ) +} + #' @export print.defaults_fn <- \(x, ...) { cat("Add defaults, e.g.: (x = 1)\n") @@ -92,6 +117,11 @@ print.converters_fn <- \(x, ...) { cat("Add converters, e.g.: (x = as_dataframe)\n") } +#' @export +print.errors_fn <- \(x, ...) { + cat("Add error handlers, e.g.: (error_handler_fn)\n") +} + #' Communicate #' #' Serve communication channels. @@ -124,6 +154,10 @@ com_serve <- \(session = shiny::getDefaultReactiveDomain()) { error = results$message, id = results$id ) + + if(env$errors[[name]]){ + env$errors[[name]](results) + } } http_response_json(results, status) @@ -164,6 +198,10 @@ com_send <- \(name, session = shiny::getDefaultReactiveDomain()) { status <- 200L if(inherits(results, "error")) { status <- 400L + if(length(env$errors[[name]])){ + env$errors[[name]](results) + } + results <- list( error = results$message, id = results$id diff --git a/README.md b/README.md index 80373e9..99156f3 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ Small framework to communicate between Shiny client and server via HTTP requests. Run `communicate::example()` for a short demo. +[Site](https://communicate.opifex.org/) + ## Installation diff --git a/docs/_coverpage.md b/docs/_coverpage.md index c81918e..bb6b0d1 100644 --- a/docs/_coverpage.md +++ b/docs/_coverpage.md @@ -9,4 +9,4 @@ [GitHub](https://github.com/devOpifex/communicate) [Get Started](/r) -![color](#3f3f3f) +![color](#fff) diff --git a/docs/index.html b/docs/index.html index 5c4d68a..3e4474e 100644 --- a/docs/index.html +++ b/docs/index.html @@ -6,7 +6,10 @@ - +
diff --git a/docs/js.md b/docs/js.md index 3f969bb..7053298 100644 --- a/docs/js.md +++ b/docs/js.md @@ -21,6 +21,8 @@ packer::npm_install("@devopifex/communicate") Ideally you'd want to catch errors as shown below, where the callback fails because there is no default for `x`. +See the R Package page to see how to handle errors R-side. + ```r library(shiny) library(communicate) diff --git a/docs/r.md b/docs/r.md index 8bb3b52..ba5e72d 100644 --- a/docs/r.md +++ b/docs/r.md @@ -143,3 +143,46 @@ server <- \(input, output, session){ shinyApp(ui, server) ``` + +## Error handling + +Finally, you can pass an error handler. + +Note that the code below purposely fails. + +```r +library(shiny) +library(communicate) + +add <- \(x, y){ + x + y + "error" # this will cause an error +} + +err <- \(error){ + cat("Aaaaah, an error!\n") + print(error) +} + +# more on JavaScript error handling in the JavaScript page +script <- " + $('#btn').on('click', () => { + communicate.com('add', {x: 1}) + .then(res => alert(`equals: ${res}`)) + .catch(error => alert('There was an error')); # catch error + }) +" + +ui <- fluidPage( + # import dependencies + useCommunicate(), + h1("Hello"), + tags$a("Communicate", id = "btn"), + tags$script(HTML(script)) +) + +server <- \(input, output, session){ + com("add", add)(x = Integer, y = Numeric)(y = 1.1)(err) +} + +shinyApp(ui, server) +``` diff --git a/man/constructors.Rd b/man/constructors.Rd index e822404..7d189a8 100644 --- a/man/constructors.Rd +++ b/man/constructors.Rd @@ -4,11 +4,14 @@ \alias{constructors} \alias{construct_defaults_fn} \alias{construct_converters_fn} +\alias{construct_errors_fn} \title{Constructors} \usage{ construct_defaults_fn(fn) construct_converters_fn(fn) + +construct_errors_fn(fn) } \description{ Adds classes to returned function to allow print method. diff --git a/srcjs/index.ts b/srcjs/index.ts index f0e66db..ce33f95 100644 --- a/srcjs/index.ts +++ b/srcjs/index.ts @@ -35,7 +35,7 @@ window.Shiny.addCustomMessageHandler( }, ); -async function com(id: string, args) { +async function com(id: string, args: any) { if (!id) { throw new Error("No id provided"); } @@ -125,7 +125,7 @@ function makeQuery(id: string, args: any): string { return argNames .map((argName) => { let arg = args[argName]; - const valid = valids.find((valid) => valid.name === argName); + const valid = valids.find((valid: any) => valid.name === argName); if (!valid) { throw new Error(