use super::balls::{Ball, parse_list};
use crate::cli_outbound::{Chunk, Cli, CliError, ExitInfo, Stream};
use balls::layout::Xdg;
use balls::reads::Catalog;
use std::io;
use std::path::{Path, PathBuf};
pub trait BlRunner: Send {
fn live(&self, project: &Path) -> io::Result<Vec<Ball>>;
fn closed(&self, project: &Path) -> io::Result<Vec<Ball>>;
fn detail(&self, project: &Path, id: &str) -> Option<Ball>;
}
pub struct BlStore {
xdg: Xdg,
cli: Cli,
}
impl BlStore {
pub fn new(xdg: Xdg, cli: Cli) -> Self {
Self { xdg, cli }
}
fn store(&self, project: &Path) -> io::Result<PathBuf> {
let clone = self.xdg.clone_dir(project);
if clone.landing().join("config").is_dir() {
Ok(clone.store())
} else {
Err(io::Error::other(format!(
"no founded balls clone for {}",
project.display()
)))
}
}
fn catalog(&self, project: &Path) -> io::Result<Catalog> {
Catalog::load(&self.store(project)?)
}
}
fn collect_stdout(stream: Result<Stream, CliError>) -> io::Result<String> {
let stream = stream.map_err(io::Error::other)?;
let mut out = Vec::new();
let mut exit = ExitInfo::Unknown;
for chunk in stream {
match chunk {
Chunk::Stdout(b) => out.extend(b),
Chunk::Stderr(_) => {}
Chunk::Exited(e) => exit = e,
}
}
match exit {
ExitInfo::Code(0) => Ok(String::from_utf8_lossy(&out).into_owned()),
other => Err(io::Error::other(format!("bl exited: {other:?}"))),
}
}
impl BlRunner for BlStore {
fn live(&self, project: &Path) -> io::Result<Vec<Ball>> {
let catalog = self.catalog(project)?;
Ok(catalog.entries().iter().map(Ball::from).collect())
}
fn closed(&self, project: &Path) -> io::Result<Vec<Ball>> {
let out = collect_stdout(
self.cli
.run_in(project, &["list", "-s", "closed", "--json"]),
)?;
Ok(parse_list(&out))
}
fn detail(&self, project: &Path, id: &str) -> Option<Ball> {
self.catalog(project).ok()?.get(id).map(Ball::from)
}
}
pub fn identity(user: Option<String>) -> String {
user.unwrap_or_default()
}
#[cfg(test)]
mod tests;