rtb-assets 0.6.4

Embedded-asset + overlay filesystem abstraction. Part of the phpboyscout Rust toolkit.
Documentation
---
title: Assets and AssetsBuilder reference
description: Every method on Assets and AssetsBuilder — signature, return value, and what happens when the input is wrong.
tags: [reference, api]
---

# `Assets` and `AssetsBuilder` reference

`Assets` is the read-only handle downstream code holds. `AssetsBuilder`
registers the layers it reads from. Both live in `rtb_assets::assets`
and are re-exported at the crate root, so `use rtb_assets::{Assets,
AssetsBuilder};` is the import you want.

Every path argument is a `&str`, not a `Path`. Asset paths are
`/`-separated strings regardless of host platform — see
[Path rules](asset-sources.md#what-paths-are-accepted).

## How do I construct an `Assets`?

```rust
let assets = rtb_assets::Assets::builder()
    // .embedded / .directory / .memory / .source, lowest priority first
    .build();
```

`Assets::builder()` and `AssetsBuilder::new()` are the same thing; the
builder derives `Default`, so `AssetsBuilder::default()` also works.
`Assets` itself derives `Default` — `Assets::default()` is a valid
handle with zero layers, from which every read returns nothing. That
is useful as a placeholder in a struct that has not been configured
yet, and it never errors.

The builder is `#[must_use]`: dropping it without calling `build()` is
a compiler warning, because a builder that is never built has done
nothing.

## What does each builder method do?

| Method | Signature | Notes |
|---|---|---|
| `embedded::<E>` | `fn embedded<E>(self, label: &'static str) -> Self where E: RustEmbed + Send + Sync + 'static` | Adds a compile-time embedded layer. `E` is your `#[derive(RustEmbed)]` type, passed by turbofish. |
| `directory` | `fn directory(self, root: impl Into<PathBuf>, label: impl Into<String>) -> Self` | Adds a layer over a directory on the host filesystem. |
| `memory` | `fn memory(self, label: impl Into<String>, files: HashMap<String, Vec<u8>>) -> Self` | Adds a layer over an in-memory map. |
| `source` | `fn source(self, source: Arc<dyn AssetSource>) -> Self` | Adds any custom layer. |
| `build` | `fn build(self) -> Assets` | Freezes the stack. |

!!! warning "`directory` takes the root first, `memory` takes the label first"
    `.directory(root, label)` and `.memory(label, files)` put the label
    in different positions. When both arguments to `.directory` are
    string literals, swapping them still compiles — `impl Into<PathBuf>`
    and `impl Into<String>` each accept a `&str` — and produces a layer
    rooted at the label, which silently reads nothing:

    ```rust
    // Wrong way round; compiles, reads nothing.
    let a = Assets::builder().directory("user", "/etc/mytool").build();
    // Debug shows the mistake: Assets { layers: ["/etc/mytool"] }
    ```

    If a directory layer contributes no files, print the `Assets` with
    `{:?}` and check the labels before you check the disk.

`label` is used only in diagnostics: it is what
[`AssetError::Parse`](errors.md#parse) names when a layer's YAML or
JSON fails to parse, and what `Debug` prints. It is not deduplicated
and it is not looked up — two layers may share a label without error.

## In what order are layers consulted?

Registration order is priority order, lowest first. The **last**
registered layer that provides a path wins a byte read; a merge folds
layers **first to last**, so later layers override earlier ones.

```rust
let assets = Assets::builder()
    .embedded::<Defaults>("defaults")   // lowest priority
    .directory("/etc/mytool", "system")
    .directory(user_dir, "user")        // highest priority
    .build();
```

[Layer precedence](../explanation/layer-precedence.md) explains why the
two read paths behave differently.

## `open` — read the winning layer's bytes

```rust
#[must_use]
pub fn open(&self, path: &str) -> Option<Vec<u8>>
```

Walks layers highest-priority first and returns the first layer's bytes.
Returns `None` when no layer provides `path`, when `path` names a
directory, and when a `DirectorySource` rejects the path as unsafe. A
missing file is not an error — the overlay model expects most layers
not to have most paths.

The returned `Vec<u8>` is a fresh copy on every call. Nothing is
cached; a `DirectorySource` re-reads from disk each time.

## `open_text` — read the winning layer's bytes as UTF-8

```rust
pub fn open_text(&self, path: &str) -> Result<String, AssetError>
```

`open` plus a UTF-8 check. Unlike `open`, a missing path is an error
here:

| Condition | Result |
|---|---|
| Some layer has `path`, bytes are valid UTF-8 | `Ok(String)` |
| No layer has `path` | `Err(AssetError::NotFound(path))` |
| Bytes are not valid UTF-8 | `Err(AssetError::NotUtf8 { path })` |

A UTF-8 BOM is not stripped — it arrives as `\u{feff}` at the start of
the string.

## `exists` — is this path provided by any layer?

```rust
#[must_use]
pub fn exists(&self, path: &str) -> bool
```

`true` if **any** layer provides `path`, not only the winning one. It
answers "can I read this", not "which layer would win".

`exists` is implemented by attempting the read and discarding the
result, so on a `DirectorySource` it costs a full file read. Prefer
`open`/`open_text` and match on the result when you are about to read
the file anyway.

`exists` returns `false` for a directory, even when `list_dir` lists
that directory as an entry of its parent.

## `list_dir` — what is directly inside this directory?

```rust
#[must_use]
pub fn list_dir(&self, dir: &str) -> Vec<String>
```

Returns the **immediate** children of `dir` across every layer:
deduplicated, byte-order sorted, and stripped of the `dir` prefix.
Nested paths are not flattened — `d/sub/b.txt` contributes `sub` to
`list_dir("d")`, not `sub/b.txt`.

```rust
// layer "lo": d/a.txt, d/sub/b.txt   layer "hi": d/b.txt
assets.list_dir("d");   // ["a.txt", "b.txt", "sub"]
```

`""` and `"."` both mean the root. A trailing slash is accepted, so
`"d"` and `"d/"` are equivalent. An unknown directory, a path that
names a file, and a path a `DirectorySource` rejects all return an
empty `Vec` rather than an error.

Two things the return value does not tell you:

- **Which entries are directories.** Names come back bare. Probe with
  `exists` — `false` for a name that appeared in the listing means it
  is a directory.
- **Which layer each entry came from.** Entries are unioned; a name
  present on three layers appears once.

Sorting is by byte value, so `README.md` sorts before `assets.rs`.

## `load_merged_yaml` / `load_merged_json` — merge a file across layers

```rust
pub fn load_merged_yaml<T: DeserializeOwned>(&self, path: &str) -> Result<T, AssetError>
pub fn load_merged_json<T: DeserializeOwned>(&self, path: &str) -> Result<T, AssetError>
```

Reads `path` from **every** layer that has it, deep-merges the results
in registration order, then deserialises the merged document into `T`.
This is the only pair of methods that does not stop at the winning
layer.

The merge rules — which values combine, which replace, and which
delete — are set out in [Merge semantics](merge-semantics.md), and
they contain at least two surprises. Read that page before you ship a
layered config file.

Failure modes, in the order they are reached:

| Condition | Result |
|---|---|
| No layer has `path` | `Err(AssetError::NotFound(path))` |
| A contributing layer's bytes are not UTF-8 (YAML only) | `Err(AssetError::Parse)`, `path` names the layer |
| A contributing layer fails to parse | `Err(AssetError::Parse)`, `path` names the layer |
| The merged document does not fit `T` | `Err(AssetError::Parse)`, `path` is the bare path |

A parse failure on any contributing layer aborts the whole load. There
is no fallback to the next layer down: a broken user override surfaces
as an error rather than silently reverting to defaults.

Fields of `T` that the merged document does not mention need a serde
default (`#[serde(default)]`, or `Option<T>`), exactly as they would
loading a single file. Unknown keys in the document are ignored unless
`T` opts into `#[serde(deny_unknown_fields)]`.

## What traits does `Assets` implement?

`Clone`, `Debug`, `Default`, `Send`, `Sync`, `'static`.

`Clone` is refcount-only — layers live behind `Arc`, so cloning an
`Assets` into a thread or a task copies two pointers and no asset
bytes. Hold it by value rather than behind a lock.

`Debug` prints layer labels lowest-priority first and nothing else:

```text
Assets { layers: ["defaults", "system", "user"] }
```

`AssetsBuilder` implements `Debug` the same way, plus `Default`.
Neither type implements `PartialEq`, `Serialize`, or `Deserialize`.

## What is deliberately not on this API?

There is no write, no delete, no create, no cache invalidation, no
file-watching, no metadata (size, mtime, mime type), no streaming
reader, and no way to ask which layer supplied a path. See
[What rtb-assets does not do](../explanation/limitations.md) for why,
and for what to reach for instead.