# save

> Keep small named pieces of text, such as settings and scores, between runs of a program.

This is the Markdown version of https://osl-lang.org/packages/save. Every page of the OSL docs has one: add .md to its path. https://osl-lang.org/llms.txt lists them all.

```osl
import "std:save"
```

`std:save` gives a program a folder of its own where it can keep named items between runs: settings, a high score, a token, the last thing the user opened. Each item is a file, written safely so a crash can't leave half of one. Use it for small per-user data. For anything you need to search or update in parts, use [db](/packages/db.md), and for files you choose the location of, use [fs](/packages/fs.md).

Items are text. To store a number, a list or a record, turn it into text with [json](/packages/json.md) on the way in and back on the way out.

## Open a store and keep an item

`save.open(appName)` returns the store for an app, creating its folder under `~/.osl/apps` when needed. `set` stores text under a name and `get` reads it back:

```osl
import "std:save"

auto store = save.open("docs-save-basics").unwrap()

// Start clean, because this example runs more than once.
for key in store.keys().unwrap() (
  store.remove(key).unwrap()
)

store.set("name", "Ada").unwrap()
log store.get("name").unwrap()

store.set("name", "Grace").unwrap()
log store.get("name").unwrap()

log store.dir().endsWith("docs-save-basics")
```

Output:

```text
Ada
Grace
true
```

Run it twice with the cleanup loop deleted: the old value is still there on the second run, because it lives on disk.

Setting a name again replaces the old value. The write goes to a temporary file first and is renamed into place, so a reader sees the old text or the new text and never a mixture.

Pick an app name that is yours alone. Two programs that open the same name share a store.

## Names and files

A name without an extension is stored as `name.txt`, so `"score"` and `"score.txt"` are the same item. Give a name an extension when the content has one, such as `settings.json`. `keys()` lists what is stored, sorted, and `path(name)` tells you which file holds an item, so other tools can open it:

```osl
import "std:save"
import "std:fs"

auto store = save.open("docs-save-keys").unwrap()
for key in store.keys().unwrap() (
  store.remove(key).unwrap()
)

store.set("note", "buy milk").unwrap()
store.set("theme.json", `{"dark": true}`).unwrap()

log store.keys().unwrap()
log store.has("note") store.has("missing")
log fs.readFile(store.path("note")).unwrap()
```

Output:

```text
["note.txt","theme.json"]
true false
buy milk
```

## Missing items

`get` and `remove` fail with a `NotFoundError` for an item that was never stored. That is a normal first-run situation, so give it a default with `unwrapOr`, or branch on `has`:

```osl
import "std:save"

auto store = save.open("docs-save-results").unwrap()
for key in store.keys().unwrap() (
  store.remove(key).unwrap()
)

string name = store.get("name").unwrapOr("stranger")
log "hello," name

log store.get("name").unwrapErr().kind

store.set("name", "Ada").unwrap()
store.remove("name").unwrap()
log store.has("name")
log store.remove("name").isErr()
```

Output:

```text
hello, stranger
NotFoundError
false
true
```

> **Warning**
>
> A name with a slash, or a trailing dot, is a programming error: `store.set("a/b", ...)` and `save.open("../x")` throw a `ValueError` and don't return a result. Names come from your code, so keep them plain.

## Saving structured data

Combine the store with `std:json` and a struct to keep a settings record. Parsing checks the stored text against the struct, so a hand-edited file with a wrong type is an error you can handle:

```osl
import "std:save"
import "std:json"

struct Settings (
  string theme = "light"
  int fontSize = 14
)

auto store = save.open("docs-save-json").unwrap()
for key in store.keys().unwrap() (
  store.remove(key).unwrap()
)

def load(*save.Store store) Settings (
  string text = store.get("settings.json").unwrapOr("{}")
  return json.parse<Settings>(text).unwrapOr(Settings())
)

Settings s = load(store)
log s.theme s.fontSize

s.theme = "dark"
s.fontSize = 16
store.set("settings.json", json.stringify(s)).unwrap()

Settings again = load(store)
log again.theme again.fontSize
log store.get("settings.json").unwrap()
```

Output:

```text
light 14
dark 16
{"theme":"dark","fontSize":16}
```

The loader falls back to a default `Settings()` in two cases: nothing is stored yet, or what is stored can't be parsed. A program that must not lose bad data should keep a copy before overwriting it.

**Quiz.** You call `store.get("score")` on a first run, before anything was saved. What do you get?

- [ ] An empty string.
- [x] A failed result with a `NotFoundError`.
- [ ] The program stops.

Answer: Missing items are normal. Use `unwrapOr` to give a default.

### Exercise: Visit counter

Count visits across runs. `visit()` reads `visits` (0 when missing), adds one, saves it and returns the new count. Call it three times.

Starting code:

```osl
import "std:save"

auto store = save.open("docs-save-files").unwrap()
for key in store.keys().unwrap() (
  store.remove(key).unwrap()
)

def visit() int (
  return 0
)

log visit() visit() visit()
```

It should print:

```text
1 2 3
```

Hint 1: Items are text. `store.get("visits").unwrapOr("0")` then `.tryAs<int>`.

Hint 2: Save with `store.set("visits", "" ++ n).unwrap()`.

A solution:

```osl
import "std:save"

auto store = save.open("docs-save-more").unwrap()
for key in store.keys().unwrap() (
  store.remove(key).unwrap()
)

def visit() int (
  int n = store.get("visits").unwrapOr("0").tryAs<int>.unwrapOr(0) + 1
  store.set("visits", "" ++ n).unwrap()
  return n
)

log visit() visit() visit()
```

## API reference

## API reference

Every member of `std:save`, generated from the source of OSL.

### Functions

#### `save.all() array`

**Deprecated.** Deprecated: use save.open(appName) and store.keys() instead.

all lists the shared store's file names.

#### `save.create() *save.Save`

**Deprecated.** Deprecated: use save.open(appName) instead.

create returns a new, uninitialised store.

#### `save.exists(string filename) boolean`

**Deprecated.** Deprecated: use save.open(appName) and store.has(name) instead.

exists reports whether the shared store has an item.

#### `save.getItem(string filename) object`

**Deprecated.** Deprecated: use save.open(appName) and store.get(name) instead.

getItem returns {app, filename, exists, data, size} from the shared
store.

#### `save.init(string appName) boolean`

**Deprecated.** Deprecated: use save.open(appName) instead.

init points the package's shared store at an app and reports whether it
worked.

#### `save.open(string appName) result<*save.Store, error>`

open returns the store for an app, creating its directory
~/.osl/apps/<appName> if needed. Throws ValueError when appName isn't a
plain file name; fails with NotFoundError when the home directory isn't
known, PermissionError or IOError.

#### `save.setItem(string filename, any value) string`

**Deprecated.** Deprecated: use save.open(appName) and store.set(name, text) instead.

setItem stores a value in the shared store and returns its path, or "".

### `*save.Store`

#### `store.dir() string`

dir returns the store's directory.

#### `store.get(string name) result<string, error>`

get returns the text stored under name. Fails with NotFoundError when
there is no such item. Throws ValueError for an invalid name.

#### `store.has(string name) boolean`

has reports whether an item is stored under name. Invalid names have no
item.

#### `store.keys() result<string[], error>`

keys lists the stored items' file names (such as "score.txt"), sorted.
Fails with NotFoundError when the store's directory has been deleted.

#### `store.path(string name) string`

path returns the file an item is stored in. A name without an extension
is stored as name.txt, so "score" and "score.txt" are the same item.
Throws ValueError for a name with slashes or a trailing ".".

#### `store.remove(string name) result<unit, error>`

remove deletes an item. Fails with NotFoundError when there is no such
item. Throws ValueError for an invalid name.

#### `store.set(string name, string text) result<unit, error>`

set stores text under name, replacing any earlier value atomically.
Throws ValueError for an invalid name; fails with PermissionError or
IOError.

### `*save.Save`

#### `save.all() array`

**Deprecated.** Deprecated: use store.keys() instead.

all lists stored file names, or [] when they can't be read.

#### `save.create() *save.Save`

**Deprecated.** Deprecated: use save.open(appName) instead.

create returns a new, uninitialised store.

#### `save.exists(string filename) boolean`

**Deprecated.** Deprecated: use store.has(name) instead.

exists reports whether an item is stored.

#### `save.getItem(string filename) object`

**Deprecated.** Deprecated: use store.get(name) and store.has(name) instead.

getItem returns {app, filename, exists, data, size} for an item.

#### `save.init(string appName) boolean`

**Deprecated.** Deprecated: use save.open(appName) instead.

init points the store at an app's directory and reports whether it
worked.

#### `save.setItem(string filename, any value) string`

**Deprecated.** Deprecated: use store.set(name, text) instead.

setItem stores a value and returns its path, or "".
