--- title: "Quick start guide" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Quick start guide} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` `potions` is a package for easily storing and retrieving information via `options()`. It therefore provides functionality somewhat similar to `{settings}`, but with syntax based more closely on `{here}`. The intended use of `potions` is for adding novel information to `options()` for use within single packages or workflows. ## Basic usage `potions` has three basic functions: - `brew()` to store data - `pour()` to retrieve data - `drain()` to clear data The first step is to store data using `brew()`, which accepts data in three formats: - Named arguments, e.g. `brew(x = 1)` - A `list`, e.g. `brew(list(x = 1))` - A configuration file, e.g. `brew(file = "my-config-file.yml")` Information stored using `brew` can be retrieved using `pour`: ```{r} library(potions) brew(x = 1) paste0("The value of x is ", pour("x")) drain() ``` ## Interactions with global options Because `potions` uses a novel `S3` object for all data storage, it **never overwrites existing global options**, and is therefore safe to use without affecting existing workflows. For example, `print.default` takes it's default `digits` argument from `getOption("digits")`: ```{r} options("digits") # set to 7 by default print(pi) ``` If we use `potions` to set `digits`, we do not affect this behaviour. Instead, the user must specifically retrieve data using `pour` for these settings to be applied: ```{r} library(potions) brew(digits = 3) print(pi, digits = pour("digits")) # using potions print(pi) # default is unaffected ``` This feature - i.e. storing data in a novel `S3` object - means that `potions` can distinguish between interactive use in the console versus being called within a package. Data can be provided and used independently by multiple packages, and in the console, without generating conflicts. Options stored using `potions` are not persistent across sessions; you will need to reload options each time you open a new workspace. It is unlikely, therefore, that you will need to 'clear' the data stored by `potions` at any point. If you do need to remove data, you can do so using `drain()` (without any further arguments). ## Using `config` files Often it is necessary to share a script, but without sharing certain sensitive information necessary to run the code. A common example is API keys or other sensitive information required to download data from a web service. In such cases, the default, interactive method of using `brew()` is insufficient, i.e. ```{r eval = FALSE} # start of script brew(list("my-secret-key" = "123456")) # shares secret information ``` To avoid this problem, you can instead supply the path to a file containing that information, i.e. ```{r eval = FALSE} brew(file = "config.yml") # hides secret information ``` You can then simply add the corresponding file name to your `gitignore`, and your script will still run, without sharing sensitive information. ## Using `potions` in package development When weighing up architectural decisions about how packages should share information between functions, there are a few solutions that developers can choose between: - Where a developer needs to be able to call static information across multiple functions, an efficient solution is to use `sysdata.rda`, which supports internal use of named objects while avoiding `options()` completely. - Where a function relies on information stored in `options()`, and for which there is no override, it is possible to temporarily reset `options()` within a function. In these cases, CRAN requires that the initial state be restored after use, for which `on.exit()` is a sensible choice (See [Advanced R section 6.7.4](https://adv-r.hadley.nz/functions.html#on-exit)). - Finally, where there is a need for dynamic, package-wide options that can be changed by the developer or the user, packages such as `potions` or [settings](https://cran.r-project.org/package=settings) can be valuable. To use `potions` in a package development situation, create a file in the `R` directory called `onLoad.R`, containing the following code: ```{r, eval=FALSE} .onLoad <- function(libname, pkgname) { if(pkgname == "packagenamehere") { potions::brew(.pkg = "packagenamehere") } } ``` This is important because it tells `potions` that you are developing a package, what that package is called, and where future calls to `brew()` from within that package should place their data. It is also possible to add defaults here, e.g. ```{r, eval=FALSE} .onLoad <- function(libname, pkgname) { if(pkgname == "packagenamehere") { potions::brew( n_attempts == 5, verbose == TRUE, .pkg = "packagenamehere") } } ``` Often when developing a package, you will want users to call your own configuration function, rather than call `brew()` directly. This provides greater control over the names & types of data stored by `potions`, which in turn gives you - the developer - greater certainty when calling those data *within* your package via `pour()`. For example, you might want to specify that a specific argument is supplied as numeric: ```{r, eval = FALSE} packagename_config <- function(fontsize = 10){ if(!is.numeric(fontsize)){ rlang::abort("Argument `fontsize` must be a number") } brew(list(fontsize = fontsize)) } ``` An additional benefit of writing a wrapper function is to allow users to provide their own `config` file. The easiest way to do this is to support a `file` argument within your own function, then pass this directly to `brew()`: ```{r, eval = FALSE} packagename_config <- function(file = NULL){ if(!is.null(file)){ brew(file = file) } } ``` This approach is risky, however, as it doesn't allow any checks. An alternative is to intercept the file, run your own checks, then pass the result to `brew()`: ```{r, eval = FALSE} packagename_config <- function(file = NULL){ if(!is.null(file)){ config_data <- potions::read_config(x) # add any checks to `data` that are needed here if(length(names(data)) != length(data)){ rlang::abort("Not all entries are named!") } # pass to `brew` brew(config_data) } } ```