use std::fs;
use std::path::Path;
use std::sync::Arc;
use tempfile::TempDir;
use crate::catalog::{Catalog, CatalogHandle, OnBroken};
use crate::config::Config;
use crate::retrieval::Retrieval;
use crate::watch::reload::{Reload, ReloadError, Reloader};
pub(super) fn prompt(name: &str, description: &str, value: &str) -> String {
let name = yaml_scalar(name);
let description = yaml_scalar(description);
let value = lua_string(value);
format!(
"---\nname: {name}\ndescription: {description}\npromptforge: 1\n---\n\n\
# Test prompt\n\n## Main\n\n```lua\nreturn {value}\n```\n"
)
}
fn yaml_scalar(value: &str) -> String {
serde_json::to_string(value).expect("a string serializes as JSON")
}
fn lua_string(value: &str) -> String {
let mut out = String::with_capacity(value.len() + 2);
out.push('"');
for ch in value.chars() {
match ch {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
other => out.push(other),
}
}
out.push('"');
out
}
pub(super) fn unparsable() -> &'static str {
"---\npromptforge: 1\n---\n\n# Test prompt\n\n## Main\n\nprose\n"
}
pub(super) fn config_source(root: &Path, extra: &str) -> String {
let prompts = toml::Value::from(root.join("prompts").display().to_string());
format!(
"[server]\ntoken = \"shared\"\n{extra}\n\n\
[gateway]\nurl = \"http://127.0.0.1:8081/v1\"\nkey = \"gw\"\n\n\
[paths]\nprompts = {prompts}\n\n\
[catalog]\ninclude = [\"*.md\"]\n",
)
}
pub(super) struct Fixture {
dir: TempDir,
pub(super) config: Arc<Config>,
reloader: Reloader,
pub(super) catalog: Arc<CatalogHandle>,
}
impl Fixture {
pub(super) fn new() -> Fixture {
Fixture::with_retrieval(|_catalog| Retrieval::idle())
}
pub(super) fn with_retrieval(build: impl FnOnce(&Catalog) -> Retrieval) -> Fixture {
let dir = tempfile::tempdir().expect("create a temporary root");
let root = dir.path();
fs::create_dir_all(root.join("prompts")).expect("create the prompts directory");
Fixture::write_prompt(root, "alpha", "Do the alpha thing", "alpha v1");
Fixture::write_prompt(root, "beta", "Do the beta thing", "beta v1");
fs::write(root.join("prompts.toml"), config_source(root, ""))
.expect("write the configuration");
let source = root.join("prompts.toml");
let config = Config::load(&source).expect("the fixture configuration loads");
let catalog = Catalog::resolve(&config, OnBroken::Reject).expect("boot resolves");
let retrieval = build(&catalog);
let catalog = Arc::new(CatalogHandle::with_retrieval(catalog, retrieval));
let config = Arc::new(config);
let reloader = Reloader::new(&source, Arc::clone(&config), Arc::clone(&catalog));
Fixture {
dir,
config,
reloader,
catalog,
}
}
pub(super) fn write_prompt(root: &Path, name: &str, description: &str, value: &str) {
fs::write(
root.join("prompts").join(format!("{name}.md")),
prompt(name, description, value),
)
.expect("write the fixture prompt");
}
pub(super) fn root(&self) -> &Path {
self.dir.path()
}
pub(super) fn rewrite(&self, name: &str, description: &str, value: &str) {
Fixture::write_prompt(self.root(), name, description, value);
}
pub(super) fn break_prompt(&self, name: &str) {
fs::write(
self.root().join("prompts").join(format!("{name}.md")),
unparsable(),
)
.expect("break the fixture prompt");
}
pub(super) fn reload(&self) -> Result<Reload, ReloadError> {
self.reloader.reload()
}
pub(super) fn reloader(&self) -> &Reloader {
&self.reloader
}
pub(super) fn description(&self, name: &str) -> String {
self.catalog
.load()
.catalog()
.find(name)
.expect("the entry is in the live catalog")
.description()
.to_owned()
}
}