use crate::{
Answer, AnswerValue, Error, ErrorKind, Provider, Question, Request, Result, State, ask,
};
use futures_util::{StreamExt, TryStreamExt, stream};
use indexmap::IndexMap;
use serde_json::Value;
use std::{collections::HashSet, num::NonZeroUsize};
#[derive(Clone)]
pub struct BatchOptions {
pub concurrency: NonZeroUsize,
pub context: Option<Value>,
#[cfg(feature = "images")]
#[cfg_attr(docsrs, doc(cfg(feature = "images")))]
pub assets: crate::image::Assets,
}
impl Default for BatchOptions {
fn default() -> Self {
Self {
concurrency: NonZeroUsize::new(8).expect("eight is positive"),
context: None,
#[cfg(feature = "images")]
assets: crate::image::Assets::default(),
}
}
}
impl std::fmt::Debug for BatchOptions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BatchOptions")
.field("concurrency", &self.concurrency)
.field("has_context", &self.context.is_some())
.finish_non_exhaustive()
}
}
#[derive(Clone)]
pub struct Item {
id: String,
state: State,
}
impl Item {
pub fn new(id: impl Into<String>, state: State) -> Result<Self> {
let id = id.into();
validate_ids([id.as_str()])?;
Ok(Self { id, state })
}
pub fn id(&self) -> &str {
&self.id
}
pub fn state(&self) -> &State {
&self.state
}
}
impl std::fmt::Debug for Item {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Item").finish_non_exhaustive()
}
}
#[derive(Clone, Debug)]
pub enum ItemResult {
Answer(Answer<AnswerValue>),
Error(Error),
}
impl ItemResult {
pub fn answer(&self) -> Option<&Answer<AnswerValue>> {
match self {
Self::Answer(a) => Some(a),
Self::Error(_) => None,
}
}
pub fn error(&self) -> Option<&Error> {
match self {
Self::Answer(_) => None,
Self::Error(e) => Some(e),
}
}
}
pub(crate) fn validate_ids<'a>(ids: impl IntoIterator<Item = &'a str>) -> Result<()> {
let mut seen = HashSet::new();
for id in ids {
if id.is_empty() || !seen.insert(id) {
return Err(Error::new(
ErrorKind::InvalidArgument,
"item identifiers must be nonempty and unique",
));
}
}
Ok(())
}
pub async fn ask_each(
provider: &dyn Provider,
question: &Question,
states: &[State],
options: &BatchOptions,
) -> Result<Vec<ItemResult>> {
let questions = IndexMap::from([("q".to_owned(), question.clone())]);
let mut requests = Vec::with_capacity(states.len());
for state in states {
let request = Request::new(state.clone(), questions.clone())?;
#[cfg(feature = "images")]
let request = {
options.assets.validate(state, &questions)?;
request.with_assets(options.assets.clone())
};
requests.push(request);
}
let mut results: Vec<(usize, ItemResult)> = stream::iter(requests.into_iter().enumerate().map(
|(position, request)| async move {
let result = match ask(provider, &request).await {
Ok(mut answers) => match answers.shift_remove("q") {
Some(answer) => ItemResult::Answer(answer),
None => ItemResult::Error(Error::new(
ErrorKind::InvalidResponse,
"provider omitted item answer",
)),
},
Err(error) => match error.kind() {
ErrorKind::Provider | ErrorKind::Timeout | ErrorKind::InvalidResponse => {
ItemResult::Error(error)
}
_ => return Err(error),
},
};
Ok((position, result))
},
))
.buffer_unordered(options.concurrency.get())
.try_collect()
.await?;
results.sort_unstable_by_key(|(position, _)| *position);
Ok(results.into_iter().map(|(_, result)| result).collect())
}