use super::{Commit, FileIo, load_snapshot, stage};
use crate::xdg::{Env, Os};
use std::path::{Path, PathBuf};
mod effects;
pub use effects::RealBzRunner;
#[cfg(test)]
mod tests;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BzOutcome {
pub success: bool,
pub stdout: String,
pub stderr: String,
}
pub trait BzRunner {
fn dump_config_at(&self, config: &Path) -> BzOutcome;
fn dump_config_effective(&self) -> BzOutcome;
fn list_models(&self, provider: &str) -> BzOutcome;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Applied {
Ok,
Rejected { stderr: String },
Conflict,
Io { error: String },
}
pub const BUILT_IN_ROWS_HINT: &str =
"six built-in provider rows are compiled into bz and are not shown in this file";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BrazenPaths {
pub config: PathBuf,
pub credentials_dir: PathBuf,
pub models_cache_dir: PathBuf,
}
impl BrazenPaths {
pub fn resolve(env: &Env, os: Os) -> Self {
Self {
config: env.brazen_config_path(),
credentials_dir: env.brazen_credentials_dir(os),
models_cache_dir: env.brazen_models_cache_dir(os),
}
}
}
#[derive(Debug, Clone)]
pub struct BrazenEditor {
paths: BrazenPaths,
draft: String,
loaded: Option<u64>,
}
impl BrazenEditor {
pub fn load(paths: BrazenPaths, io: &dyn FileIo) -> std::io::Result<Self> {
let (draft, loaded) = load_snapshot(io, &paths.config)?;
Ok(Self {
paths,
draft,
loaded,
})
}
#[cfg(test)]
pub(crate) fn draft(&self) -> &str {
&self.draft
}
pub(crate) fn draft_mut(&mut self) -> &mut String {
&mut self.draft
}
pub fn set_draft(&mut self, text: String) {
self.draft = text;
}
pub fn reload(&mut self, io: &dyn FileIo) -> std::io::Result<()> {
let (draft, loaded) = load_snapshot(io, &self.paths.config)?;
self.draft = draft;
self.loaded = loaded;
Ok(())
}
pub fn apply(&mut self, runner: &dyn BzRunner, io: &dyn FileIo) -> Applied {
match self.apply_inner(runner, io) {
Ok(applied) => applied,
Err(e) => Applied::Io {
error: e.to_string(),
},
}
}
fn apply_inner(&mut self, runner: &dyn BzRunner, io: &dyn FileIo) -> std::io::Result<Applied> {
let staged = stage(io, &self.paths.config, self.draft.as_bytes())?;
let gate = runner.dump_config_at(staged.temp());
if !gate.success {
staged.discard(io)?;
return Ok(Applied::Rejected {
stderr: gate.stderr,
});
}
match staged.commit(io, self.loaded)? {
Commit::Ok(hash) => {
self.loaded = Some(hash);
Ok(Applied::Ok)
}
Commit::Conflict => Ok(Applied::Conflict),
}
}
pub fn effective(&self, runner: &dyn BzRunner) -> BzOutcome {
runner.dump_config_effective()
}
pub fn credential_presence(&self, io: &dyn FileIo) -> Vec<(String, bool)> {
provider_names(&self.draft)
.into_iter()
.map(|name| {
let path = self.paths.credentials_dir.join(format!("{name}.json"));
let present = io.exists(&path);
(name, present)
})
.collect()
}
pub fn model_cache(&self, provider: &str, io: &dyn FileIo) -> std::io::Result<Option<String>> {
let path = self.paths.models_cache_dir.join(format!("{provider}.json"));
Ok(io
.read(&path)?
.map(|b| String::from_utf8_lossy(&b).into_owned()))
}
pub fn refresh_models(&self, provider: &str, runner: &dyn BzRunner) -> BzOutcome {
runner.list_models(provider)
}
}
pub(crate) fn provider_names(text: &str) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
for line in text.lines() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("name")
&& let Some(value) = rest.trim_start().strip_prefix('=')
&& let Some(name) = quoted_value(value.trim())
&& !out.contains(&name)
{
out.push(name);
}
}
out
}
fn quoted_value(s: &str) -> Option<String> {
let inner = s.strip_prefix('"')?;
let end = inner.find('"')?;
inner.get(..end).map(str::to_string)
}