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.
How do I construct an Assets?¶
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. |
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:
// 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 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.
let assets = Assets::builder()
.embedded::<Defaults>("defaults") // lowest priority
.directory("/etc/mytool", "system")
.directory(user_dir, "user") // highest priority
.build();
Layer precedence explains why the two read paths behave differently.
open — read the winning layer's bytes¶
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¶
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?¶
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?¶
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.
// 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—falsefor 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¶
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, 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:
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 for why, and for what to reach for instead.