Skip to main content

agent_plugins/
source.rs

1//! A convenience layer over the sans-IO machine for synchronous sources.
2//!
3//! Implement [`Source`] for anything that can answer the loader's four
4//! questions — a directory, an archive, a key-value map — and [`load`] runs
5//! the whole conversation for you. Async IO needs no trait at all: drive
6//! [`Loading`](crate::Loading) directly and `.await` between answers.
7
8use crate::load::{Answer, DirEntry, FileKind, LoadedPlugin, Loading, Question, Rejection};
9use crate::path::PackagePath;
10
11/// A synchronous supplier of plugin package bytes.
12pub trait Source {
13    /// The source's own failure type. Failures are folded into the loading
14    /// conversation as [`Answer::Unavailable`], where the spec's failure
15    /// boundaries decide how far the damage spreads.
16    type Error: std::fmt::Display;
17
18    /// What kind of entry does `path` resolve to? `None` if absent.
19    fn kind(&mut self, path: &PackagePath) -> Result<Option<FileKind>, Self::Error>;
20
21    /// Does `path`, fully resolved, stay within the plugin root? Sources
22    /// that cannot contain links may simply return `Ok(true)`.
23    fn confined(&mut self, path: &PackagePath) -> Result<bool, Self::Error>;
24
25    /// Read the file at `path`.
26    fn read(&mut self, path: &PackagePath) -> Result<Vec<u8>, Self::Error>;
27
28    /// List the immediate entries of the directory at `path`.
29    fn list(&mut self, path: &PackagePath) -> Result<Vec<DirEntry>, Self::Error>;
30}
31
32/// Load a plugin by running the full loading conversation against a source.
33pub fn load<S: Source>(source: &mut S) -> Result<LoadedPlugin, Rejection> {
34    let mut loading = Loading::start();
35    while let Some(question) = loading.question() {
36        let answer = match &question {
37            Question::Kind(path) => match source.kind(path) {
38                Ok(kind) => Answer::Kind(kind),
39                Err(e) => Answer::Unavailable { detail: e.to_string() },
40            },
41            Question::Confined(path) => match source.confined(path) {
42                Ok(confined) => Answer::Confined(confined),
43                Err(e) => Answer::Unavailable { detail: e.to_string() },
44            },
45            Question::Read(path) => match source.read(path) {
46                Ok(bytes) => Answer::Bytes(bytes),
47                Err(e) => Answer::Unavailable { detail: e.to_string() },
48            },
49            Question::List(path) => match source.list(path) {
50                Ok(entries) => Answer::Entries(entries),
51                Err(e) => Answer::Unavailable { detail: e.to_string() },
52            },
53        };
54        loading.answer(answer).expect("answer variant matches the question just asked");
55    }
56    loading.finish()
57}