use std::pin::Pin;
use std::task::{Context, Poll};
use bytes::Bytes;
use futures_core::Stream;
use serde::{Deserialize, Serialize};
use crate::api::messages::MessagesRequest;
use crate::client::Client;
use crate::error::{Error, Result};
use crate::types::{string_enum, tagged_enum, Message};
string_enum! {
pub enum BatchStatus {
InProgress = "in_progress",
Canceling = "canceling",
Ended = "ended",
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct BatchRequestCounts {
#[serde(default)]
pub processing: u64,
#[serde(default)]
pub succeeded: u64,
#[serde(default)]
pub errored: u64,
#[serde(default)]
pub canceled: u64,
#[serde(default)]
pub expired: u64,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct MessageBatch {
pub id: String,
#[serde(rename = "type")]
pub object_type: String,
pub processing_status: BatchStatus,
pub request_counts: BatchRequestCounts,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub created_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ended_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cancel_initiated_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub archived_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub results_url: Option<String>,
#[serde(flatten)]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct BatchPage {
pub data: Vec<MessageBatch>,
#[serde(default)]
pub has_more: bool,
#[serde(default)]
pub first_id: Option<String>,
#[serde(default)]
pub last_id: Option<String>,
}
#[derive(Clone, Debug, Default)]
pub struct BatchListParams {
pub after_id: Option<String>,
pub before_id: Option<String>,
pub limit: Option<u32>,
}
impl BatchListParams {
fn to_query(&self) -> Vec<(&'static str, String)> {
let mut query = Vec::new();
if let Some(limit) = self.limit {
query.push(("limit", limit.to_string()));
}
if let Some(after_id) = &self.after_id {
query.push(("after_id", after_id.clone()));
}
if let Some(before_id) = &self.before_id {
query.push(("before_id", before_id.clone()));
}
query
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct BatchRequestItem {
pub custom_id: String,
pub params: serde_json::Value,
}
impl BatchRequestItem {
pub fn new(custom_id: impl Into<String>, params: serde_json::Value) -> Self {
Self {
custom_id: custom_id.into(),
params,
}
}
pub fn from_request(custom_id: impl Into<String>, request: &MessagesRequest) -> Result<Self> {
Ok(Self {
custom_id: custom_id.into(),
params: request.to_json_body()?,
})
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct BatchSucceeded {
pub message: Message,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct BatchErrored {
pub error: serde_json::Value,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct BatchCanceled {}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct BatchExpired {}
tagged_enum! {
#[allow(clippy::large_enum_variant)]
pub enum BatchResultOutcome {
Succeeded(BatchSucceeded) = "succeeded",
Errored(BatchErrored) = "errored",
Canceled(BatchCanceled) = "canceled",
Expired(BatchExpired) = "expired",
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct BatchResult {
pub custom_id: String,
pub result: BatchResultOutcome,
}
pub struct BatchResults {
#[cfg(not(target_arch = "wasm32"))]
inner: Pin<Box<dyn Stream<Item = reqwest::Result<Bytes>> + Send>>,
#[cfg(target_arch = "wasm32")]
inner: Pin<Box<dyn Stream<Item = reqwest::Result<Bytes>>>>,
buffer: Vec<u8>,
finished: bool,
}
impl BatchResults {
pub(crate) fn new(response: reqwest::Response) -> Self {
Self {
inner: Box::pin(response.bytes_stream()),
buffer: Vec::new(),
finished: false,
}
}
fn parse_line(line: &[u8]) -> Option<Result<BatchResult>> {
let trimmed = trim_ascii(line);
if trimmed.is_empty() {
return None;
}
Some(serde_json::from_slice::<BatchResult>(trimmed).map_err(Error::from))
}
}
impl Stream for BatchResults {
type Item = Result<BatchResult>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
loop {
if let Some(position) = this.buffer.iter().position(|&byte| byte == b'\n') {
let line: Vec<u8> = this.buffer.drain(..=position).collect();
match BatchResults::parse_line(&line) {
Some(item) => return Poll::Ready(Some(item)),
None => continue,
}
}
if this.finished {
if !this.buffer.is_empty() {
let line = std::mem::take(&mut this.buffer);
if let Some(item) = BatchResults::parse_line(&line) {
return Poll::Ready(Some(item));
}
}
return Poll::Ready(None);
}
match this.inner.as_mut().poll_next(cx) {
Poll::Ready(Some(Ok(bytes))) => {
this.buffer.extend_from_slice(&bytes);
continue;
}
Poll::Ready(Some(Err(source))) => {
this.finished = true;
return Poll::Ready(Some(Err(Error::from(source))));
}
Poll::Ready(None) => {
this.finished = true;
continue;
}
Poll::Pending => return Poll::Pending,
}
}
}
}
fn trim_ascii(mut slice: &[u8]) -> &[u8] {
while let [first, rest @ ..] = slice {
if first.is_ascii_whitespace() {
slice = rest;
} else {
break;
}
}
while let [rest @ .., last] = slice {
if last.is_ascii_whitespace() {
slice = rest;
} else {
break;
}
}
slice
}
#[derive(Clone, Copy, Debug)]
pub struct Batches<'a> {
client: &'a Client,
}
impl<'a> Batches<'a> {
pub(crate) fn new(client: &'a Client) -> Self {
Self { client }
}
pub async fn create(&self, requests: &[BatchRequestItem]) -> Result<MessageBatch> {
let body = serde_json::json!({ "requests": requests });
self.client
.http()
.post_json("/v1/messages/batches", &body, &[])
.await
}
pub async fn get(&self, id: &str) -> Result<MessageBatch> {
let path = format!("/v1/messages/batches/{id}");
self.client.http().get_json(&path, &[]).await
}
pub async fn list(&self, params: &BatchListParams) -> Result<BatchPage> {
let query = params.to_query();
self.client
.http()
.get_json_query("/v1/messages/batches", &query, &[])
.await
}
pub async fn cancel(&self, id: &str) -> Result<MessageBatch> {
let path = format!("/v1/messages/batches/{id}/cancel");
self.client.http().post_no_body(&path, &[]).await
}
pub async fn results(&self, id: &str) -> Result<BatchResults> {
let path = format!("/v1/messages/batches/{id}/results");
let response = self.client.http().get_raw(&path, &[]).await?;
Ok(BatchResults::new(response))
}
}