In a hidden chunk here, I “export” the internal helpers covered below.

User-facing messages

Everything should be emitted by helpers in utils-ui.R, such as gs4_success() or gs4_info(). These are all wrappers around cli::cli_alert() and friends.

gs4_success("Doing good stuff")
#> ✔ Doing good stuff
gs4_info("The more you know!")
#> ℹ The more you know!
gs4_warning("You might want to know about this")
#> ! You might want to know about this
gs4_danger("Something quite bad")
#> ✖ Something quite bad

The helpers encourage consistent styling and make it possible to selectively silence messages coming from googlesheets4. The googlesheets4 message helpers:

  • Use the cli package to get interpolation, inline markup, and pluralization
  • Eventually route through rlang::inform(), which is important because inform() prints to standard output in interactive sessions. This means that informational messages won’t have the same “look” as errors and can generally be more stylish, at least in IDEs like RStudio.
  • Are under the control of the GOOGLESHEETS4_QUIET environment variable. If it’s unset, the default is to show messages (unless we’re testing, i.e. the environment variable TESTTHAT is "true"). GOOGLESHEETS4_QUIET=true will suppress messages. There are withr-style convenience helpers: local_gs4_quiet() and with_gs4_quiet().

Inline styling

How we use the inline classes:

  • .file for the name of Google Sheet
  • .field for the name of a worksheet
  • .field for an A1-style range or named range
  • .url and .email for a URL or email address

These may not demo well via pkgdown, but the interactive experience is nice.

nm <- "name-of-a-Google-Sheet"
gs4_success("Creating new Sheet: {.file {nm}}")
#> ✔ Creating new Sheet: name-of-a-Google-Sheet

nm <- "name-of-a-worksheet"
gs4_success("Protecting cells on sheet: {.field {nm}}")
#> ✔ Protecting cells on sheet: name-of-a-worksheet

rg <- "A3:B20"
gs4_success("Writing to the range {.field {rg}}")
#> ✔ Writing to the range A3:B20

Most relevant cli docs:

Line breaks and whitespace

Because cli wants to deal with whitespace and wrapping, for better or for worse, lines breaks and leading whitespace in the source have no effect. This is different from glue::glue(), which we use in error messages (see below), and it’s easy to get confused.

things <- "rows"
rg <- "A3:B20"
gs4_success(
  "Resizing one
or more {things} in
    {.field {rg}}")
#> ✔ Resizing one or more rows in A3:B20

Pluralization

cli’s pluralization is awesome!

nm <- "name-of-a-Google-Sheet"
n_new <- 1
gs4_success("Adding {n_new} sheet{?s} to {.file {nm}}")
#> ✔ Adding 1 sheet to name-of-a-Google-Sheet

n_new <- 3
gs4_success("Adding {n_new} sheet{?s} to {.file {nm}}")
#> ✔ Adding 3 sheets to name-of-a-Google-Sheet

Collapsing

Collapsing lists of things is great! Also more pluralization.

new_sheet_names <- c("apple", "banana", "cherry")
gs4_success("New sheet{?s}: {.field {new_sheet_names}}")
#> ✔ New sheets: apple, banana, and cherry

new_sheet_names <- "kumquat"
gs4_success("New sheet{?s}: {.field {new_sheet_names}}")
#> ✔ New sheet: kumquat

Tricky stuff

If you want to see some tricky examples of building up a message from parts, look here:

Errors

Use gs4_abort() instead of rlang::abort() or stop(). So far, I’m not really using ... to put data in the condition, but I could start when/if there’s a reason to. Be prepared to get confused about how to style and line break error messages vs. regular messages, which use glue::glue() and cli::cli_alert(), respectively.

abort_unsupported_conversion() is a wrapper around gs4_abort().

x <- structure(1, class = c("a", "b", "c"))
abort_unsupported_conversion(x, to = "foofy")
#> Error: Don't know how to make an instance of <character> from something of class <a/b/c>

abort_unsupported_conversion() exists to standardize a recurring type of error message, usually encountered during development, not by end-users. I use it a lot in the default method of an as_{to}() generic.

Inline styling

We process the message of gs4_abort() with glue::glue().

Use helpers dq(), sq(), and bt() for inline style (yes, it’s clunky compared to cli):

  • dq() for double quotes (Sheet names)
  • sq() for single quotes (worksheet names, ranges, and most strings, generally)
  • bt() for backticks (argument names, functions)
nm <- "name-of-a-Google-Sheet"
gs4_abort("Use double quotes around a Google Sheet name: {dq(nm)}")
#> Error: Use double quotes around a Google Sheet name: "name-of-a-Google-Sheet"

nm <- "name-of-a-worksheet"
gs4_abort("Use single quotes around a worksheet name: {sq(nm)}")
#> Error: Use single quotes around a worksheet name: 'name-of-a-worksheet'

gs4_abort("In fact, when in doubt, just use single quotes around {sq('stuff')}")
#> Error: In fact, when in doubt, just use single quotes around 'stuff'

gs4_abort("
  But use backticks when referring to an {bt('argument')} \\
  or {bt('function()')}")
#> Error: But use backticks when referring to an `argument` or `function()`

Multiple lines and bullets

Get multiple lines by sending a character vector as message. Think about what rlang::format_error_bullets() does and be intentional with naming: the choices are i, x, and no name.

bad_stuff <- c("eew", "yuck", "uh-oh")
gs4_abort(c(
  "This first line is a header, explaining the general situation",
  x = "Sometimes you have to deliver Very Bad News",
  bad_stuff,
  i = "Maybe a call to {bt('magic_function()')} would help?"
))
#> Error: This first line is a header, explaining the general situation
#> ✖ Sometimes you have to deliver Very Bad News
#> * eew
#> * yuck
#> * uh-oh
#> ℹ Maybe a call to `magic_function()` would help?

endpoint <- "sheets.spreadsheets.WTF"
gs4_abort(c("Endpoint not recognized:", x = "{sq(endpoint)}"))
#> Error: Endpoint not recognized:
#> ✖ 'sheets.spreadsheets.WTF'

Line breaks and whitespace

Remember each element of message is processed with glue::glue(), so let’s review the line break situation, which is different to the messages made with cli, which is confusing.

This produces two lines:

shift <- "SHIFT"
gs4_abort("
  The {bt('shift')} direction must be specified for this {bt('range')}
  It can't be automatically determined")
#> Error: The `shift` direction must be specified for this `range`
#> It can't be automatically determined

Use \\ if you want continuation, i.e. you want one line:

shift <- "SHIFT"
gs4_abort("
  The {bt('shift')} direction must be specified for this {bt('range')}. \\
  It can't be automatically determined.")
#> Error: The `shift` direction must be specified for this `range`. It can't be automatically determined.

very_very_very_very_very_very_very_very_long_variable_name <- "HA HA"
gs4_abort(c(
  "Imagine {very_very_very_very_very_very_very_very_long_variable_name} \\
   a line that's long in source but short after interpolation", 
  x = "Short bad thing",
  i = "Helpful tip"
))
#> Error: Imagine HA HA a line that's long in source but short after interpolation
#> ✖ Short bad thing
#> ℹ Helpful tip

Pluralization

Use cli::pluralize() directly if you need pluralization in error messages. Search for cli::qty() for a few places where the pluralization was a bit trickier.

col_names <- c("apple", "banana")
nc <- 1
gs4_abort(c(
  "Length of {bt('col_names')} is not compatible with the data:",
  x = cli::pluralize("Expected {length(col_names)} un-skipped column{?s}"),
  x = cli::pluralize("But data has {nc} column{?s}")
))
#> Error: Length of `col_names` is not compatible with the data:
#> ✖ Expected 2 un-skipped columns
#> ✖ But data has 1 column

m <- 8
sheets_df <- head(iris)
gs4_abort(c(
  cli::pluralize("There {?is/are} {nrow(sheets_df)} sheet{?s}:"),
  x = "Requested sheet number is out-of-bounds: {m}"
))
#> Error: There are 6 sheets:
#> ✖ Requested sheet number is out-of-bounds: 8

sheets_df <- head(iris, 1)
gs4_abort(c(
  cli::pluralize("There {?is/are} {nrow(sheets_df)} sheet{?s}:"),
  x = "Requested sheet number is out-of-bounds: {m}"
))
#> Error: There is 1 sheet:
#> ✖ Requested sheet number is out-of-bounds: 8

Collapsing

Use glue::glue_collapse() ahead of time, if you need to style and collapse.

bad_codes <- letters[1:3]
bad_codes <- glue::glue_collapse(sq(bad_codes), sep = ",")
gs4_abort(c(
  "{bt('col_types')} must be a string of readr-style shortcodes:",
  x = "Unrecognized codes: {bad_codes}"
))
#> Error: `col_types` must be a string of readr-style shortcodes:
#> ✖ Unrecognized codes: 'a','b','c'

Tricky stuff

If you want to see some tricky examples of building up a message from parts, look here:

  • rep_ctypes() (conditional pluralization)

Future thoughts

Would I ever want to use cli in the errors? There are good things and bad things about that. Messing around. This may very well not show up in pkgdown, which is related to what’s problematic.

rlang::abort(cli::col_blue("Hello ", "world!"))
#> Error: Hello world!

rlang::abort(paste("... to highlight the", cli::col_red("search term"),
                   "in a block of text\n"))
#> Error: ... to highlight the search term in a block of text

error <- cli::combine_ansi_styles("red", "bold")
warn <- cli::combine_ansi_styles("magenta", "underline")
note <- cli::style_italic

rlang::abort(error("Error: subscript out of bounds!\n"))
#> Error: Error: subscript out of bounds!
rlang::abort(warn("Warning: shorter argument was recycled.\n"))
#> Error: Warning: shorter argument was recycled.
rlang::abort(note("Note: no such directory.\n"))
#> Error: Note: no such directory.