R/messages.R

Defines functions error_if warn_if info_if error warn info

Documented in error error_if info info_if warn warn_if

#  FUNCTION: info --------------------------------------------------------------
#
#' Display a message, and record it in a log file.
#'
#' `info()` is similar to [message()], but it also writes the message to a log
#' file. Whether it is shown, or written to the log, depends on the level and
#' type of the message. See details below for more information.
#'
#' Whether a message is shown, or written to the log, depends on two options:
#'
#' 1. **Level**: This allows control over the depth of messages. Each message
#'    can be assigned a `level` and if it is below the `msg_level` (set in the
#'    package option `msgr.level` by default) the message is displayed and
#'    written to the log.
#' 2. **Type**: The type is refers to whether the message is "INFO", "WARNING"
#'    or "ERROR", as produced by the functions [info()], [warn()] and [error()]
#'    respectively. If the message type is in the `msg_types` (set in the
#'    package option `msgr.types` by default) the message is displayed and
#'    written to the log. This allows you to for instance, just display errors
#'    and warnings and ignore messages.
#'
#' The location of the log file is set in the package option `msgr.log_path`, or
#' as an argument to this function. messages are added with a time stamp. If the
#' `log_path` is equal to "" then no log is produced.
#'
#' @param ... (strings) message to be displayed or written to file.
#' @param level (integer, optional) The level of the message, from 1 to 10.
#'   Default: 1.
#' @param msg_level (integer, optional) The maximum level of messages to output.
#'   Default: set in the option `"msgr.level"`.
#' @param msg_types (character, optional) The type to write or display. Must
#'   either NULL or one or more from "INFO", "WARNING" or "ERROR". Default: set
#'   in the option `"msgr.types"`.
#' @param log_path (string, optional) The file path to the text log file. If set
#'   to "", then no logs are written. Default: set in the option
#'   `"msgr.log_path"`.
#'
#' @return A string is return invisibly containing the message.
#'
#' @examples
#' # Use info() to create timed messages
#' info("This is a simple message")
#' info("This is a level 2 message, so not shown by default", level = 2)
#'
#' # Set default level in options to determine what is shown
#' options(msgr.level = 2)
#' info("This is a level 2 message, so is shown now", level = 2)
#'
#' # Set message types in options to determine what is shown
#' options(msgr.types = c("WARNING", "ERROR"))
#' info("This is message, so will not be shown now")
#'
#' @export
#'
info <- function(
  ...,
  level     = 1,
  msg_level = getOption("msgr.level"),
  msg_types = getOption("msgr.types"),
  log_path  = getOption("msgr.log_path")
) {
  is_natural(level, n = 1) && is_in_range(level, min = 1, max = 10) ||
    stop("'level' must be an integer between 1 and 10: ", level)
  is_natural(msg_level, n = 1) && is_in_range(msg_level, min = 1, max = 10) ||
    stop("'msg_level' must be an integer between 1 and 10: ", msg_level)
  is.null(msg_types) || is.character(msg_types) ||
    stop("'msg_types' must be NULL or a character vector: ", msg_types)
  all(is_in(msg_types, c("INFO", "WARNING", "ERROR"))) ||
    stop("'msg_types' must be either 'INFO', 'WARNING' or 'ERROR': ", msg_types)
  is.character(log_path) && length(log_path) == 1 ||
    stop("'log_path' must be a string: ", log_path)

  msg <- paste0(...)

  if ("INFO" %in% msg_types && level <= msg_level) {
    if (log_path != "") {
      log_path <- fs::path_expand(log_path)

      if (!fs::is_dir(fs::path_dir(log_path))) {
        fs::dir_create(fs::path_dir(log_path), recurse = TRUE)
      }

      if (!fs::is_file(log_path)) {
        fs::file_create(log_path)
      }

      write(paste0(Sys.time(), " | INFO | ", msg), log_path, append = TRUE)
    }

    message(paste0("[", format(Sys.time(), "%H:%M:%S"), "] ", msg))
  }

  invisible(msg)
}

#  FUNCTION: warn --------------------------------------------------------------
#
#' Display a warning, and record it in a log file.
#'
#' `warn()` is similar to [warning()], but it also writes the warning to a log
#' file. Whether it is shown, or written to the log, depends on the level and
#' type of the warning. See details below for more information.
#'
#' Whether a warning is shown, or written to the log, depends on two options:
#'
#' 1. **Level**: This allows control over the depth of messages. Each message
#'    can be assigned a `level` and if it is below the `msg_level` (set in the
#'    package option `msgr.level` by default) the message is displayed and
#'    written to the log.
#' 2. **Type**: The type is refers to whether the message is "INFO", "WARNING"
#'    or "ERROR", as produced by the functions [info()], [warn()] and [error()]
#'    respectively. If the message type is in the `msg_types` (set in the
#'    package option `msgr.types` by default) the message is displayed and
#'    written to the log. This allows you to for instance, just display errors
#'    and warnings and ignore messages.
#'
#' The location of the log file is set in the package option `msgr.log_path`, or
#' as an argument to this function. messages are added with a time stamp. If the
#' `log_path` is equal to "" then no log is produced.
#'
#' @param ... (strings) warning to be displayed or written to file.
#' @param level (integer, optional) The level of the warning, from 1 to 10.
#'   Default: 1.
#' @param msg_level (integer, optional) The maximum level of messages to output.
#'   Default: set in the option `"msgr.level"`.
#' @param msg_types (character, optional) The type to write or display. Must
#'   either NULL or one or more from "INFO", "WARNING" or "ERROR". Default: set
#'   in the option `"msgr.types"`.
#' @param log_path (string, optional) The file path to the text log file. If set
#'   to "", then no logs are written. Default: set in the option
#'   `"msgr.log_path"`.
#'
#' @return A string is return invisibly containing the warning
#'
#' @examples
#' # Use warn() to create timed warnings
#' warn("This is a simple warning")
#' warn("This is a level 2 warning, so not shown by default", level = 2)
#'
#' # Set default level in options to determine what is shown
#' options(msgr.level = 2)
#' warn("This is a level 2 warning, so is shown now", level = 2)
#'
#' # Set message types in options to determine what is shown
#' options(msgr.types = "ERROR")
#' warn("This is warning, so will not be shown now")
#'
#' @export
#'
warn <- function(
  ...,
  level     = 1,
  msg_level = getOption("msgr.level"),
  msg_types = getOption("msgr.types"),
  log_path  = getOption("msgr.log_path")
) {
  is_natural(level, n = 1) && is_in_range(level, min = 1, max = 10) ||
    stop("'level' must be an integer between 1 and 10: ", level)
  is_natural(msg_level, n = 1) && is_in_range(msg_level, min = 1, max = 10) ||
    stop("'msg_level' must be an integer between 1 and 10: ", msg_level)
  is.null(msg_types) || is.character(msg_types) ||
    stop("'msg_types' must be NULL or a character vector: ", msg_types)
  all(is_in(msg_types, c("INFO", "WARNING", "ERROR"))) ||
    stop("'msg_types' must be either 'INFO', 'WARNING' or 'ERROR': ", msg_types)
  is.character(log_path) && length(log_path) == 1 ||
    stop("'log_path' must be a string: ", log_path)

  msg <- paste0(...)

  if ("WARNING" %in% msg_types && level <= msg_level) {
    if (log_path != "") {
      log_path <- fs::path_expand(log_path)

      if (!fs::is_dir(fs::path_dir(log_path))) {
        fs::dir_create(fs::path_dir(log_path), recurse = TRUE)
      }

      if (!fs::is_file(log_path)) {
        fs::file_create(log_path)
      }

      write(paste0(Sys.time(), " | WARNING | ", msg), log_path, append = TRUE)
    }

    warning(
      paste0("[", format(Sys.time(), "%H:%M:%S"), "] ", msg),
      call.      = FALSE,
      immediate. = TRUE
    )
  }

  invisible(msg)
}

#  FUNCTION: error -------------------------------------------------------------
#
#' Display an error, and record it in a log file.
#'
#' `error()` is similar to [stop()], but it also writes the error to a log file.
#' Whether it is shown, or written to the log, depends on the level and type of
#' the error. See details below for more information.
#'
#' Whether an error is shown, or written to the log, depends on two options:
#'
#' 1. **Level**: This allows control over the depth of messages. Each message
#'    can be assigned a `level` and if it is below the `msg_level` (set in the
#'    package option `msgr.level` by default) the message is displayed and
#'    written to the log.
#' 2. **Type**: The type is refers to whether the message is "INFO", "WARNING"
#'    or "ERROR", as produced by the functions [info()], [warn()] and [error()]
#'    respectively. If the message type is in the `msg_types` (set in the
#'    package option `msgr.types` by default) the message is displayed and
#'    written to the log. This allows you to for instance, just display errors
#'    and warnings and ignore messages.
#'
#' The location of the log file is set in the package option `msgr.log_path`, or
#' as an argument to this function. messages are added with a time stamp. If the
#' `log_path` is equal to "" then no log is produced.
#'
#' @param ... (strings) error to be displayed or written to file.
#' @param level (integer, optional) The level of the error, from 1 to 10.
#'   Default: 1.
#' @param msg_level (integer, optional) The maximum level of messages to output.
#'   Default: set in the option `"msgr.level"`.
#' @param msg_types (character, optional) The type to write or display. Must
#'   either NULL or one or more from "INFO", "WARNING" or "ERROR". Default: set
#'   in the option `"msgr.types"`.
#' @param log_path (string, optional) The file path to the text log file. If set
#'   to "", then no logs are written. Default: set in the option
#'   `"msgr.log_path"`.
#'
#' @return A string is return invisibly containing the error
#'
#' @examples \dontrun{
#'
#'   # Use error() to create timed errors
#'   error("This is a simple error")
#'   error("This is a level 2 error, so not shown by default", level = 2)
#'
#'   # Set default level in options to determine what is shown
#'   options(msgr.level = 2)
#'   error("This is a level 2 error, so is shown now", level = 2)
#'
#' }
#'
#' @export
#'
error <- function(
  ...,
  level     = 1,
  msg_level = getOption("msgr.level"),
  msg_types = getOption("msgr.types"),
  log_path  = getOption("msgr.log_path")
) {
  is_natural(level, n = 1) && is_in_range(level, min = 1, max = 10) ||
    stop("'level' must be an integer between 1 and 10: ", level)
  is_natural(msg_level, n = 1) && is_in_range(msg_level, min = 1, max = 10) ||
    stop("'msg_level' must be an integer between 1 and 10: ", msg_level)
  is.null(msg_types) || is.character(msg_types) ||
    stop("'msg_types' must be NULL or a character vector: ", msg_types)
  all(is_in(msg_types, c("INFO", "WARNING", "ERROR"))) ||
    stop("'msg_types' must be either 'INFO', 'WARNING' or 'ERROR': ", msg_types)
  is.character(log_path) && length(log_path) == 1 ||
    stop("'log_path' must be a string: ", log_path)

  msg <- paste0(...)

  if ("ERROR" %in% msg_types && level <= msg_level) {
    if (log_path != "") {
      log_path <- fs::path_expand(log_path)

      if (!fs::is_dir(fs::path_dir(log_path))) {
        fs::dir_create(fs::path_dir(log_path), recurse = TRUE)
      }

      if (!fs::is_file(log_path)) {
        fs::file_create(log_path)
      }

      write(paste0(Sys.time(), " | ERROR | ", msg), log_path, append = TRUE)
    }

    stop(paste0("[", format(Sys.time(), "%H:%M:%S"), "] ", msg), call. = FALSE)
  }

  invisible(msg)
}

# FUNCTION: info_if ------------------------------------------------------------
#
#' Display a message, and record in a log file, if a condition is true.
#'
#' This function calls the [info()] function to display a message if the
#' specified condition is true. If a message is not specified then a generic
#' message is displayed.
#'
#' @param condition (boolean) The condition to check.
#' @param ... (strings) message to be displayed or written to file.
#' @param level (integer, optional) The level of the message, from 1 to 10.
#'   Default: 1.
#' @param msg_level (integer, optional) The maximum level of messages to output.
#'   Default: set in the option `"msgr.level"`.
#' @param msg_types (character, optional) The type to write or display. Must
#'   either NULL or one or more from "INFO", "WARNING" or "ERROR". Default: set
#'   in the option `"msgr.types"`.
#' @param log_path (string, optional) The file path to the text log file. If set
#'   to "", then no logs are written. Default: set in the option
#'   `"msgr.log_path"`.
#'
#' @return A string is return invisibly containing the message.
#'
#' @examples
#' # Use info_if() to create conditional timed messages
#' info_if(2 > 1, "Condition is true so this message is shown")
#' info_if(1 > 2, "Condition is false so this message is not shown")
#'
#' # As with info() a level can be set
#' info_if(2 > 1, "This is a level 2 message, so not shown", level = 2)
#'
#' # Set default level in options to determine what is shown
#' options(msgr.level = 2)
#' info_if(2 > 1, "This is a level 2 message, so is shown now", level = 2)
#'
#' # Set message types in options to determine what is shown
#' options(msgr.types = c("WARNING", "ERROR"))
#' info_if(2 > 1, "This is message, so will not be shown now")
#'
#' @export
#'
info_if <- function(
  condition,
  ...,
  level     = 1,
  msg_level = getOption("msgr.level"),
  msg_types = getOption("msgr.types"),
  log_path  = getOption("msgr.log_path")
) {
  is_natural(level, n = 1) && is_in_range(level, min = 1, max = 10) ||
    stop("'level' must be an integer between 1 and 10: ", level)
  is_natural(msg_level, n = 1) && is_in_range(msg_level, min = 1, max = 10) ||
    stop("'msg_level' must be an integer between 1 and 10: ", msg_level)
  is.null(msg_types) || is.character(msg_types) ||
    stop("'msg_types' must be NULL or a character vector: ", msg_types)
  all(is_in(msg_types, c("INFO", "WARNING", "ERROR"))) ||
    stop("'msg_types' must be either 'INFO', 'WARNING' or 'ERROR': ", msg_types)
  is.character(log_path) && length(log_path) == 1 ||
    stop("'log_path' must be a string: ", log_path)

  if (isTRUE(condition)) {
    uneval_condition <- substitute(condition)

    prefix <- ""
    if (sys.nframe() > 1) {
      calling_function <- deparse(sys.calls()[[sys.nframe() - 1]][[1]])
      prefix <- paste0("In ", calling_function, "(): ")
    }

    msg <- list(...)
    if (length(msg) == 0) {
      msg <- paste(deparse(uneval_condition), "is true")
    } else {
      msg <- paste(as.character(msg), collapse = "")
    }

    info(
      prefix, msg,
      level     = level,
      msg_level = msg_level,
      msg_types = msg_types,
      log_path  = log_path
    )
  }
}

# FUNCTION: warn_if ------------------------------------------------------------
#
#' Display a warning, and record in a log file, if a condition is true.
#'
#' This function calls the [warn()] function to display a warning if the
#' specified condition is true. If a message is not specified then a generic
#' message is displayed.
#'
#' @param condition (boolean) The condition to check.
#' @param ... (strings) message to be displayed or written to file.
#' @param level (integer, optional) The level of the message, from 1 to 10.
#'   Default: 1.
#' @param msg_level (integer, optional) The maximum level of messages to output.
#'   Default: set in the option `"msgr.level"`.
#' @param msg_types (character, optional) The type to write or display. Must
#'   either NULL or one or more from "INFO", "WARNING" or "ERROR". Default: set
#'   in the option `"msgr.types"`.
#' @param log_path (string, optional) The file path to the text log file. If set
#'   to "", then no logs are written. Default: set in the option
#'   `"msgr.log_path"`.
#'
#' @return A string is return invisibly containing the warning.
#'
#' @examples
#' # Use warn_if() to create conditional timed warnings
#' warn_if(2 > 1, "Condition is true so this warning is shown")
#' warn_if(1 > 2, "Condition is false so this warning is not shown")
#'
#' # As with warn() a level can be set
#' warn_if(2 > 1, "This level 2 warning is not shown by default", level = 2)
#'
#' # Set default level in options to determine what is shown
#' options(msgr.level = 2)
#' warn_if(2 > 1, "This level 2 warning is shown now", level = 2)
#'
#' # Set message types in options to determine what is shown
#' options(msgr.types = "ERROR")
#' warn_if(2 > 1, "This is warning, so will not be shown now")
#'
#' @export
#'
warn_if <- function(
  condition,
  ...,
  level     = 1,
  msg_level = getOption("msgr.level"),
  msg_types = getOption("msgr.types"),
  log_path  = getOption("msgr.log_path")
) {
  is_natural(level, n = 1) && is_in_range(level, min = 1, max = 10) ||
    stop("'level' must be an integer between 1 and 10: ", level)
  is_natural(msg_level, n = 1) && is_in_range(msg_level, min = 1, max = 10) ||
    stop("'msg_level' must be an integer between 1 and 10: ", msg_level)
  is.null(msg_types) || is.character(msg_types) ||
    stop("'msg_types' must be NULL or a character vector: ", msg_types)
  all(is_in(msg_types, c("INFO", "WARNING", "ERROR"))) ||
    stop("'msg_types' must be either 'INFO', 'WARNING' or 'ERROR': ", msg_types)
  is.character(log_path) && length(log_path) == 1 ||
    stop("'log_path' must be a string: ", log_path)

  if (isTRUE(condition)) {
    uneval_condition <- substitute(condition)

    prefix <- ""
    if (sys.nframe() > 1) {
      calling_function <- deparse(sys.calls()[[sys.nframe() - 1]][[1]])
      prefix <- paste0("In ", calling_function, "(): ")
    }

    msg <- list(...)
    if (length(msg) == 0) {
      msg <- paste(deparse(uneval_condition), "is true")
    } else {
      msg <- paste(as.character(msg), collapse = "")
    }

    warn(
      prefix, msg,
      level     = level,
      msg_level = msg_level,
      msg_types = msg_types,
      log_path  = log_path
    )
  }
}

# FUNCTION: error_if -----------------------------------------------------------
#
#' Display an error, and record in a log file, if a condition is true.
#'
#' This function calls the [error()] function to display an error if the
#' specified condition is true. If a message is not specified then a generic
#' message is displayed.
#'
#' @param condition (boolean) The condition to check.
#' @param ... (strings) message to be displayed or written to file.
#' @param level (integer, optional) The level of the message, from 1 to 10.
#'   Default: 1.
#' @param msg_level (integer, optional) The maximum level of messages to output.
#'   Default: set in the option `"msgr.level"`.
#' @param msg_types (character, optional) The type to write or display. Must
#'   either NULL or one or more from "INFO", "WARNING" or "ERROR". Default: set
#'   in the option `"msgr.types"`.
#' @param log_path (string, optional) The file path to the text log file. If set
#'   to "", then no logs are written. Default: set in the option
#'   `"msgr.log_path"`.
#'
#' @return A string is return invisibly containing the error
#'
#' @examples \dontrun{
#'
#'   # Use error_if() to create conditional timed errors
#'   error_if(2 > 1, "Condition is true so this error is shown")
#'   error_if(1 > 2, "Condition is false so this error is not shown")
#'
#'   # As with error() a level can be set
#'   error_if(2 > 1, "This level 2 error is not shown by default", level = 2)
#'
#'   # Set default level in options to determine what is shown
#'   options(msgr.level = 2)
#'   error_if(2 > 1, "This is a level 2 error, so is shown now", level = 2)
#'
#' }
#'
#' @export
#'
error_if <- function(
  condition,
  ...,
  level     = 1,
  msg_level = getOption("msgr.level"),
  msg_types = getOption("msgr.types"),
  log_path  = getOption("msgr.log_path")
) {
  is_natural(level, n = 1) && is_in_range(level, min = 1, max = 10) ||
    stop("'level' must be an integer between 1 and 10: ", level)
  is_natural(msg_level, n = 1) && is_in_range(msg_level, min = 1, max = 10) ||
    stop("'msg_level' must be an integer between 1 and 10: ", msg_level)
  is.null(msg_types) || is.character(msg_types) ||
    stop("'msg_types' must be NULL or a character vector: ", msg_types)
  all(is_in(msg_types, c("INFO", "WARNING", "ERROR"))) ||
    stop("'msg_types' must be either 'INFO', 'WARNING' or 'ERROR': ", msg_types)
  is.character(log_path) && length(log_path) == 1 ||
    stop("'log_path' must be a string: ", log_path)

  if (isTRUE(condition)) {
    uneval_condition <- substitute(condition)

    prefix <- ""
    if (sys.nframe() > 1) {
      calling_function <- deparse(sys.calls()[[sys.nframe() - 1]][[1]])
      prefix <- paste0("In ", calling_function, "(): ")
    }

    msg <- list(...)
    if (length(msg) == 0) {
      msg <- paste(deparse(uneval_condition), "is true")
    } else {
      msg <- paste(as.character(msg), collapse = "")
    }

    error(
      prefix, msg,
      level     = level,
      msg_level = msg_level,
      msg_types = msg_types,
      log_path  = log_path
    )
  }
}
ChadGoymer/msgr documentation built on April 10, 2021, 10:31 a.m.