rtb-assets 0.6.4

Embedded-asset + overlay filesystem abstraction. Part of the phpboyscout Rust toolkit.
Documentation
---
title: AssetError reference
description: Every AssetError variant, the diagnostic code it carries, the message it renders, and what causes it.
tags: [reference, errors, diagnostics]
---

# `AssetError` reference

`AssetError` is the only error type the crate produces. It is
`#[non_exhaustive]`, derives `thiserror::Error` and
`miette::Diagnostic`, and every variant carries a stable diagnostic
code under the `rtb::assets::` namespace.

```rust
#[non_exhaustive]
pub enum AssetError {
    NotFound(String),
    NotUtf8 { path: String },
    Parse { path: String, format: &'static str, message: String },
}
```

Because the enum is `#[non_exhaustive]`, a `match` on it needs a `_`
arm; a new variant in a future release will not break your build.

Only three methods return a `Result`: `open_text`, `load_merged_yaml`
and `load_merged_json`. `open`, `exists` and `list_dir` cannot fail —
they return `None`, `false` and an empty `Vec` respectively.

## `NotFound`

```text
asset not found: templates/readme.md
```

Diagnostic code `rtb::assets::not_found`. The payload is the requested
path, unchanged.

Raised when **no** registered layer provides the path. Causes, in
rough order of likelihood:

- The path does not match how the asset is keyed. Embedded and
  in-memory layers compare keys literally, so a leading `./`, a leading
  `/`, or a backslash separator will not match.
- The layer you expected to hold it was never registered, or was
  registered with its arguments swapped — print the `Assets` with
  `{:?}` and check the labels.
- A `DirectorySource` root does not exist. That is not an error in
  itself; the layer simply contributes nothing.
- The path names a directory. Directories are listable, not readable.

## `NotUtf8`

```text
asset `icons/app.png` is not valid UTF-8
```

Diagnostic code `rtb::assets::not_utf8`. Raised only by `open_text`,
when a layer provided bytes that are not valid UTF-8.

The bytes are discarded rather than lossily converted. Read binary
assets with `open`, which returns `Option<Vec<u8>>` and never inspects
the contents.

A UTF-8 byte-order mark does not trigger this — the BOM is valid UTF-8
and arrives as a leading `\u{feff}` in the returned `String`. Strip it
yourself if the consumer of the string will not tolerate it.

## `Parse`

```text
failed to parse asset `cfg.yaml (layer `user`)` as YAML: did not find expected key
```

Diagnostic code `rtb::assets::parse`, with the help text "verify the
file is well-formed YAML" (or JSON). Three fields:

| Field | Contents |
|---|---|
| `path` | either `<path> (layer `<label>`)` for a per-layer failure, or the bare `<path>` for a failure of the merged result |
| `format` | `"YAML"` or `"JSON"` |
| `message` | the underlying parser's message, verbatim |

Raised in three distinct situations, which the `path` field
distinguishes:

1. **A contributing layer's file is malformed.** `path` names the
   layer. The load aborts; there is no fallback to lower layers.
2. **A contributing layer's YAML bytes are not UTF-8.** Also names the
   layer, also reported as a parse failure rather than `NotUtf8`.
3. **The merged document does not fit `T`.** `path` is bare, because
   merging has already happened and no single layer can be blamed. A
   missing required field, a number out of range for its type, or a
   type mismatch all land here.

The `path` field is a human-readable description, not a path you can
feed back into `open`. If you need the clean path for a retry or a log
field, keep hold of the string you passed in.

## Rendering an `AssetError`

`Display` gives the one-line message above. `miette`'s report handler
gives the code and help text as well, which is what a CLI built on the
toolkit will print:

```rust
match assets.load_merged_yaml::<Config>("config.yaml") {
    Ok(config) => config,
    Err(e) => return Err(miette::Report::new(e)),
}
```

The diagnostic codes are part of the public contract and are safe to
match on or document in a tool's own troubleshooting guide.

## What is deliberately not an error

- **A missing layer root.** A `DirectorySource` pointed at a directory
  the user has not created behaves as an empty layer.
- **A rejected path.** `../../etc/passwd` returns `None` from `open`
  and `NotFound` from `open_text` — the same as any absent file, with
  no indication that a security rule fired. See
  [Why path traversal is rejected lexically]../explanation/path-safety.md
  for the reasoning.
- **An unreadable file.** Permission denied is indistinguishable from
  absent, and a lower layer gets its turn.
- **An empty overlay.** `Assets::default()` is valid and every read
  simply finds nothing.