BristolMyersSquibb / blockr

Composable, extensible no-code UI
https://bristolmyerssquibb.github.io/blockr/
GNU General Public License v3.0
34 stars 3 forks source link

No-fix validation #376

Closed nbenn closed 5 months ago

nbenn commented 5 months ago

Instead of trying to resolve inconsistencies that might arise from changes upstream in a stack by resetting incompatible values to defaults, we now fail validation and require user feedback to resolve issues.

DivadNojnarg commented 5 months ago

@nbenn We probably should not call validate_field inside initialize_field? See https://github.com/blockr-org/blockr/pull/376/files#diff-8546c791c7ba260a074d6a5270c42f3e71c421dd507effb82059def8f8316d53R63.

This currently returns NULL.

nbenn commented 5 months ago

@DivadNojnarg agreed. I modified initialize_field() and update_field() a bit. Not sure if this is what we want exactly, but it should be better than what we had.

DivadNojnarg commented 5 months ago

@nbenn. One remark about validation_failure: calling rlang::abort causes the app to crash. It should probably be handled differently, with rlang::warn as I did in 981f306. Then on the shiny side, req ensures we stop the evaluation if the validation failed. Finally, the message is extracted and sent to the UI.

DivadNojnarg commented 5 months ago

@nbenn update on where I am.

Currently, in the block server function, we execute the validation which in turns allows to do the computation but only for the current block.

Problem:

An issue that arises when having multiple blocks: assuming a stack with a data block and a select block, both valid. Empty the field from the data block, block 1 becomes invalid but somehow, the block2 server functions is triggered again and block2 appears valid because it still has the old values from the previous data. This fails because the columns don't exist in the new expression.

A piece of info is missing at the stack level.

Possible solution:

From the stack level, we should probably pass the validation state of the previous block and add a check at the top level of the block server function: req(previous_block_valid) so that this server function does not go further.

nbenn commented 5 months ago

@DivadNojnarg many thanks!

Regarding stop() vs warning(), that was done on purpose. If executed in a tryCatch with a handler for validation_failure (as is the case for is_valid.field()), this should not cause the app to crash, cf

is_valid(new_string_field())

throws and catches the error.

The reason for not using warnings is that as soon as a validation failure is encountered, validation stops. This way you can have conditions is a sequence, where the next is only evaluated when the previous passes. An example for what I mean

validate <- function(val, fun) {
  if (length(val) != 1)
    fun("expecting a string")
  if (!val %in% c("foo", "bar"))
    fun("expecting certain values")
  invisible(NULL)
}

validate("foo", stop)
validate(character(), stop)
validate(character(), warning)

With warnings you have no control over the error that is ultimately thrown.

I'll try to find out why the error is not properly handled from within the shiny app. Do you know, is error handling in shiny different from base R?

DivadNojnarg commented 5 months ago

@nbenn Commit e05ff8d fixes the error issue. No crash anymore. Now I need to solve this.

DivadNojnarg commented 5 months ago

@nbenn That's where we currently are. Resetting the data/changing dataset seems to work for basic blocks.

https://github.com/blockr-org/blockr/assets/18291543/fe0a17ed-079a-472a-b53f-843608d13b58

Few issues:

DivadNojnarg commented 5 months ago

Digging in the list_field issue, when I create a filter block and putting a browser within ui_update.list_field, I don't see any sub fields:

x
$value
NULL

$sub_fields
character(0)

[[3]]
function(data, columns) {
    determine_field <- function(x) {
      switch(class(x),
        factor = new_select_field,
        numeric = new_range_field,
        new_string_field
      )
    }

    field_args <- function(x) {
      switch(class(x),
        factor = list(levels(x)[1L], choices = levels(x)),
        numeric = list(range(x), min = min(x), max = max(x)),
        list()
      )
    }

    cols <- data[, columns, drop = FALSE]

    ctor <- lapply(cols, determine_field)
    args <- lapply(cols, field_args)

    Map(do.call, ctor, args)
  }
<bytecode: 0x11f2706d0>
<environment: 0x10ce1b440>

attr(,"type")
[1] "literal"
attr(,"class")
[1] "list_field" "field"     
attr(,"title")
[1] "Value"
attr(,"descr")
[1] ""
attr(,"status")
[1] "active"
attr(,"exclude")
[1] FALSE

which means, nothing can be inserted.

JohnCoene commented 5 months ago

Apologies if it is not the best place for comments, let me know if there is a better place.

This is the example I have in #378

my_block <- function(...){
  all_cols <- function(data) colnames(data)

  first_col <- \(data) colnames(data)[1]
  second_col <- \(data) colnames(data)[2]

  fields <- list(
    x_var = new_select_field(first_col, all_cols),
    y_var = new_select_field(second_col, all_cols)
  )

  new_block(
    fields = fields,
    expr = quote({
      ggplot2::ggplot(
        data,
        mapping = ggplot2::aes_string(
          x = .(x_var),
          y = .(y_var)
        )
      ) +
        ggplot2::geom_point() +
        ggplot2::geom_line()
    }),
    ...,
    class = "my_block"
  )
}

.S3method("server_output", "my_block", function(x, result, output){
  renderPlot(result())
})

.S3method("uiOutputBlock", "my_block", function(x, ns){
  plotOutput(ns("res"))
})

# we have to implement this
.S3method("generate_server", "my_block", function(...){
  blockr:::generate_server_block(...)
})

stack <-  new_stack(
  new_dataset_block,
  my_block
)

ui <- fluidPage(
  theme = bslib::bs_theme(5L),
  generate_ui(stack)
)

server <- function(input, output, session) {
  generate_server(stack)
}

shinyApp(ui, server)

In "My block" I cannot change any of the inputs, the inputs reset, perhaps I'm not doing this right, or it may be linked to one of the issues pointed out by @DivadNojnarg?

issue

EDIT:

DivadNojnarg commented 5 months ago

@nbenn, after digging a bit more, I noticed that get_sub_fields(x) always return NULL, which is an issue as it is used within value<-.list_field:

`value<-.list_field` <- function(x, name = "value", value) {
  set_sub_fields(x, Map(`value<-`, get_sub_fields(x), name, value))
}

This means, Map(value<-, get_sub_fields(x), name, value) does not yield the expected output, so is set_sub_fields.

DivadNojnarg commented 5 months ago

@nbenn regarding the select input validation issue, it is coming from is_string:

is_string <- function(x) {
  is.character(x) && is_scalar(x)
}

When passed multiple values, is_scalar is FALSE.

The select validation is:

validate_field.select_field <- function(x) {
  val <- value(x)
  validate_string(val)
...
}

Initial state, we have character(0) which fails the validation, as expected. Then, as soon as we select a value from the select input, we'd have to apply validate_string to any element (side note: not necessary in Shiny's context as the selectInput does not allow to pass other things than strings in the UI and returns strings):

This would lead to something like:

if (length(val) <= 1) {
    validate_string(val)
  } else {
   # useless in Shiny' context
    lapply(val, validate_string)
  }

The else being "useless" as explained before. Maybe this is still relevant as we plan to initialise blocks outside the stack (outside shiny' context).

nbenn commented 5 months ago

@JohnCoene Yes, validation_failure() should absolutely be an exported function and this is what should be used in block validators. Sorry, this was an oversight on my end and should be fixed now.

nbenn commented 5 months ago

@DivadNojnarg fixed the select field validation (3f5cc7a). Thanks for reporting & debugging.

nbenn commented 5 months ago

@DivadNojnarg one problem with the list field value() and value<-() implementations was that what I had was only correct when manipulating the "value" component and not any other (such as the "sub_fields" component).

Still not quite there yet, as changes are currently being overwritten. Looking into that now.

nbenn commented 5 months ago

@DivadNojnarg I believe the list field issue is now resolved. The value<-() replacement function is super ugly however. Currently the mechanics for these recursive fields is unsatisfyingly complex. We might want to re-think that (but not as part of this PR as unrelated).

nbenn commented 5 months ago

@JohnCoene thanks for pointing out some issues you ran into.

JohnCoene commented 5 months ago

I see these issues in this block for instance.

test_block <- function(...){
  first <- \() letters[1]
  all <- \() letters

  fields <- list(
    text = new_string_field("default"),
    select = new_select_field(first, all),
    bool = new_switch_field(TRUE),
    numeric = new_numeric_field(1L, min = 0, max = 10L),
    submit = new_submit_field(),
    browser = new_filesbrowser_field(),
    slider = new_range_field(1, 0, 10),
    range = new_range_field(c(1,2), 0, 10)
  )

  new_block(
    fields = fields,
    expr = quote(iris),
    ...,
    class = c("data_block", "my_block")
  )
}