R/utils.R

Defines functions format_difftime_ms format_difftime now_utc package_name list_call dquote str_extract read_lines_if_exists wait_until check_port free_port is_pkgload_package json_parse_extract pkgload_loaded package_file_root bind_args formals_required parse_mime parse_path_parameters set_names squote lock_bindings is_directory to_json_string to_json vcapply vlapply system_file `%||%`

`%||%` <- function(a, b) { # nolint
  if (is.null(a)) b else a
}


system_file <- function(...) {
  system.file(..., mustWork = TRUE)
}


vlapply <- function(x, fun, ...) {
  vapply(x, fun, logical(1), ...)
}


vcapply <- function(x, fun, ...) {
  vapply(x, fun, character(1), ...)
}


## This should probably be tuneable?
to_json <- function(x) {
  jsonlite::toJSON(x, json_verbatim = TRUE, na = "null", null = "null")
}


to_json_string <- function(x) {
  as.character(to_json(x))
}


is_directory <- function(x) {
  file.info(x, extra_cols = FALSE)$isdir
}


lock_bindings <- function(names, e) {
  for (nm in names) {
    lockBinding(nm, e)
  }
}


squote <- function(x) {
  sprintf("'%s'", x)
}


set_names <- function(x, nms) {
  names(x) <- nms
  x
}


## NOTE: plumber does not expose a parser here so we just bodge one
## together
parse_path_parameters <- function(x) {
  p <- strsplit(x, "/", fixed = TRUE)[[1]]
  i <- grepl("^<([^>]+)>$", p)
  name <- p[i]
  if (length(name) == 0L) {
    return(NULL)
  }
  name <- substr(name, 2L, nchar(name) - 1L)
  type <- rep("string", length(name))

  j <- grepl(":", name, fixed = TRUE)

  translate <- list(
    "logical" = c("bool", "boolean", "logical"),
    "integer" = c("int", "integer"),
    "numeric" = c("dbl", "double", "float", "number", "numeric"),
    "string" = c("chr", "str", "character", "string"))
  translate <- set_names(rep(names(translate), lengths(translate)),
                         unlist(translate, FALSE, FALSE))

  if (any(j)) {
    type[j] <- unname(translate[sub(".*:", "", name[j])])
    type[is.na(type)] <- "string"
    name[j] <- sub(":.*", "", name[j])
  }

  cbind(name = name, type = type)
}


## There's a bunch to do here with subtypes and encodings - how we
## read text will depend on that.
##
## Doing this properly requires the following:
##   https://mimesniff.spec.whatwg.org/#parsing-a-mime-type
##
## See also http://pretty-rfc.herokuapp.com/RFC2616#media.types
##
## I'm just punting on this now.
parse_mime <- function(mime) {
  if (is.null(mime)) {
    return(NULL)
  }
  if (grepl(";", mime, fixed = TRUE)) {
    ## Discard all parameters for now - later we might use these if we
    ## have text/plain inputs with encoding to get right.
    mime <- sub("\\s*;.*", "", mime)
  }
  parts <- strsplit(mime, "/", fixed = TRUE)[[1L]]
  type <- parts[[1L]]
  subtype <- parts[[2L]]
  is_text <- type == "text" ||
    (type == "application" && subtype %in% c("xml", "json"))
  list(mime = mime,
       type = type,
       subtype = subtype,
       is_text = is_text)
}


## Just a utility that works with an argslist and works out if they
## are missing or not.
formals_required <- function(f) {
  args <- formals(f)
  required <- logical(length(args))
  for (i in seq_along(args)) {
    x <- args[[i]]
    required[[i]] <- missing(x)
  }
  names(args)[required]
}


## This can be done better - I don't love the .target name at all -
## but it will work at least, and the interface is good.
bind_args <- function(target, data) {
  args <- formals(target)
  keep <- args[!(names(args) %in% names(data))]

  env <- list2env(data, new.env(parent = baseenv()))
  env$.target <- target
  body <- call("{", as.call(c(list(quote(.target)),
                              lapply(names(args), as.name))))

  as.function(c(keep, body), env)
}


## Find the root for included files (not including system files) for a
## package. This corresponds to the "inst" directory.  This is
## different when the package load has been simulated with
## pkgload. This will come in useful in both tests and documentation.
##
## There are 4 cases here:
##
## porcelain real    + package real    => system.file is correct
## porcelain pkgload + package real    => system.file is correct
## porcelain real    + package pkgload => system.file is incorrect
## porcelain pkgload + package pkgload => system.file is correct
package_file_root <- function(package) {
  path <- system.file(package = package, mustWork = TRUE)
  if (pkgload_loaded() &&
      pkgload::is_dev_package(package) &&
      !pkgload::is_dev_package("porcelain")) {
    path <- file.path(path, "inst")
  }
  path
}


pkgload_loaded <- function() {
  "pkgload" %in% loadedNamespaces()
}


json_parse_extract <- function(json, name) {
  cache$v8$call("jsonParseExtract", json, name)
}


is_pkgload_package <- function(name) {
  !is.null(name) &&
     "pkgload" %in% loadedNamespaces() &&
     pkgload::is_dev_package(name)
}


free_port <- function(min, max, attempts = 20) {
  r <- max - min + 1L
  for (i in seq_len(attempts)) {
    p <- min + sample.int(r, 1L) - 1L
    if (check_port(p)) {
      return(p)
    }
  }
  stop(sprintf("Did not find a free port between %d..%d in %d attempts",
               min, max, attempts),
       call. = FALSE)
}


check_port <- function(port, timeout = 0.1) {
  con <- tryCatch(suppressWarnings(socketConnection(
    "localhost", port = port, timeout = timeout, open = "r")),
    error = function(e) NULL)
  if (is.null(con)) {
    return(TRUE)
  }
  close(con)
  FALSE
}


wait_until <- function(condition, timeout = 10, poll = timeout / 100,
                       verbose = FALSE, title = NULL) {
  t_start <- Sys.time()
  t_quit <- t_start + timeout
  now <- t_start
  if (verbose) {
    message(title %||% "Waiting ")
  }
  repeat {
    if (condition()) {
      break
    }
    if (verbose) {
      message(".", appendLF = FALSE)
    }
    Sys.sleep(poll)
    now <- Sys.time()
    if (now > t_quit) {
      stop("Timeout reached")
    }
  }
  if (verbose) {
    elapsed_s <- round(as.numeric(now - t_start, "secs"), 1)
    message(sprintf("...OK in %s s", elapsed_s))
  }
  now - t_start
}


read_lines_if_exists <- function(path) {
  if (file.exists(path)) {
    readLines(path)
  }
}


str_extract <- function(string, start, len) {
  substr(string, start, start + len - 1L)
}


dquote <- function(x) {
  sprintf('"%s"', x)
}


list_call <- function(fn, x) {
  n <- length(x)
  prefix <- "  "
  for (i in seq_along(x)) {
    el <- paste0(prefix, x[[i]])
    end <- if (i == n) ")" else ","
    el[[length(el)]] <- paste0(el[[length(el)]], end)
    x[[i]] <- el
  }

  c(paste0(fn, "("), unlist(x))
}


package_name <- function(env) {
  utils::packageName(env)
}

now_utc <- function() {
  as.POSIXct(Sys.time(), tz = "UTC")
}

## Modified version of difftime to show timings
## in ms and to a precision
format_difftime <- function(time1, time2) {
  z <- as.numeric(time1 - time2, "secs")
  units <- if (!is.finite(z) || z <= 1) {
    "ms"
  } else if (z < 60) {
    "secs"
  } else if (z < 3600) {
    "mins"
  } else {
    "hours"
  }
  units_difftime <- if (units == "ms") "secs" else units
  z <- as.numeric(time1 - time2, units_difftime)
  if (units == "ms") {
    z <- z * 1000
    sprintf("%.0f %s", z, units)
  } else {
    sprintf("%.2f %s", z, units)
  }
}

format_difftime_ms <- function(time1, time2) {
  z <- as.numeric(time1 - time2, "secs")
  z <- z * 1000
  round(z, digits = 0)
}
reside-ic/porcelain documentation built on March 4, 2024, 11:11 p.m.