use crate::cli_outbound::{
Cli, Streamed, StreamedLine, StreamedOutcome, StreamedPoll, stderr_text, stdout_text,
};
const LIST_FLAG: &str = "--list-models";
const PROVIDER_FLAG: &str = "--provider";
const JSON_FLAG: &str = "--json";
pub const EMPTY_ROSTER: &str = "the provider offered no models";
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RosterView {
pub in_flight: bool,
pub models: Vec<String>,
pub error: Option<String>,
pub fallback: Option<String>,
}
pub struct Roster {
streamed: Option<Streamed>,
view: RosterView,
lines: Vec<StreamedLine>,
provider: String,
argv: Vec<String>,
}
pub fn start(bz: &Cli, provider: &str) -> Roster {
let args = [LIST_FLAG, PROVIDER_FLAG, provider, JSON_FLAG];
let mut argv = vec![bz.binary().display().to_string()];
argv.extend(args.iter().map(|s| (*s).to_string()));
let mut roster = Roster {
streamed: None,
view: RosterView::default(),
lines: Vec::new(),
provider: provider.to_owned(),
argv,
};
match bz.run(&args) {
Ok(stream) => {
roster.streamed = Some(Streamed::new(stream));
roster.view.in_flight = true;
}
Err(spawn) => roster.fail(spawn.to_string()),
}
roster
}
impl Roster {
pub fn provider(&self) -> String {
self.provider.clone()
}
pub fn view(&self) -> RosterView {
self.view.clone()
}
pub fn poll(&mut self) -> bool {
let Some(streamed) = self.streamed.as_mut() else {
return false;
};
match streamed.poll() {
StreamedPoll::Lines(lines) => {
self.lines.extend(lines);
true
}
StreamedPoll::Pending => true,
StreamedPoll::Done(outcome) => {
self.settle(outcome);
false
}
}
}
fn settle(&mut self, outcome: StreamedOutcome) {
self.lines.extend(outcome.lines);
self.streamed = None;
self.view.in_flight = false;
if outcome.exit != 0 {
let stderr = stderr_text(&self.lines).trim().to_string();
let why = if stderr.is_empty() {
format!("bz {LIST_FLAG} exited {}", outcome.exit)
} else {
stderr
};
return self.fail(why);
}
let models = model_ids(&stdout_text(&self.lines));
if models.is_empty() {
return self.fail(EMPTY_ROSTER.to_string());
}
self.view.models = models;
}
fn fail(&mut self, why: String) {
self.streamed = None;
self.view.in_flight = false;
self.view.error = Some(why);
self.view.fallback = Some(self.argv.join(" "));
}
#[cfg(test)]
pub(crate) fn from_streamed(streamed: Streamed, provider: &str) -> Self {
Self {
streamed: Some(streamed),
view: RosterView {
in_flight: true,
..RosterView::default()
},
lines: Vec::new(),
provider: provider.to_owned(),
argv: vec!["bz".to_string(), LIST_FLAG.to_string()],
}
}
}
fn rows(document: &str) -> Vec<serde_json::Value> {
serde_json::from_str::<serde_json::Value>(document.trim())
.ok()
.and_then(|listing| {
listing
.get("models")
.and_then(serde_json::Value::as_array)
.cloned()
})
.unwrap_or_default()
}
pub fn model_ids(stdout: &str) -> Vec<String> {
rows(stdout)
.iter()
.filter_map(|row| row.get("id").and_then(serde_json::Value::as_str))
.map(str::to_owned)
.collect()
}