use std::collections::BTreeMap;
use std::convert::Infallible;
use crate::load::{DirEntry, FileKind, LoadedPlugin, Rejection};
use crate::path::PackagePath;
use crate::source::{self, Source};
#[derive(Debug, Clone, Default)]
pub struct MemSource {
files: BTreeMap<String, Vec<u8>>,
}
impl MemSource {
pub fn new() -> Self {
Self::default()
}
pub fn file(mut self, path: impl Into<String>, bytes: impl Into<Vec<u8>>) -> Self {
self.files.insert(path.into(), bytes.into());
self
}
pub fn load(mut self) -> Result<LoadedPlugin, Rejection> {
source::load(&mut self)
}
}
impl Source for MemSource {
type Error = Infallible;
fn kind(&mut self, path: &PackagePath) -> Result<Option<FileKind>, Infallible> {
let path = path.as_str();
if self.files.contains_key(path) {
return Ok(Some(FileKind::File));
}
let prefix = format!("{path}/");
Ok(self.files.keys().any(|k| k.starts_with(&prefix)).then_some(FileKind::Directory))
}
fn confined(&mut self, _path: &PackagePath) -> Result<bool, Infallible> {
Ok(true) }
fn read(&mut self, path: &PackagePath) -> Result<Vec<u8>, Infallible> {
Ok(self.files.get(path.as_str()).cloned().unwrap_or_default())
}
fn list(&mut self, path: &PackagePath) -> Result<Vec<DirEntry>, Infallible> {
let prefix = format!("{}/", path.as_str());
let mut names: Vec<DirEntry> = Vec::new();
for key in self.files.keys() {
let Some(rest) = key.strip_prefix(&prefix) else {
continue;
};
let (name, kind) = match rest.split_once('/') {
Some((dir, _)) => (dir, FileKind::Directory),
None => (rest, FileKind::File),
};
if names.last().map(|e: &DirEntry| e.name.as_str()) != Some(name) {
names.push(DirEntry::new(name, kind));
}
}
Ok(names) }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::load::McpOutcome;
const MANIFEST: &[u8] = br#"{
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name": "mem-plugin"
}"#;
#[test]
fn loads_skills_and_ignores_deeper_nesting() {
let plugin = MemSource::new()
.file("plugin.json", MANIFEST)
.file(
"skills/greet/SKILL.md",
b"---\nname: greet\ndescription: Greets. Use when greeting.\n---\nSay hi.\n"
.to_vec(),
)
.file(
"skills/greet/nested/SKILL.md",
b"---\nname: nested\ndescription: d\n---\n".to_vec(),
)
.file("skills/loose-file.md", b"not a skill".to_vec())
.load()
.unwrap();
assert_eq!(plugin.skills.len(), 1);
assert_eq!(plugin.skills[0].meta.name, "greet");
assert_eq!(plugin.mcp, McpOutcome::Absent);
assert!(plugin.diagnostics.is_empty());
}
#[test]
fn missing_manifest_rejects() {
let outcome = MemSource::new().load();
assert!(matches!(outcome, Err(Rejection::ManifestMissing)));
}
}