use tracing::{debug, trace, warn};
use super::{request::*, response::*};
use crate::{
ZaiResult,
client::{ZaiClient, error::codes},
};
pub struct FileParseResultRequest {
task_id: String,
}
impl FileParseResultRequest {
pub fn new(task_id: impl Into<String>) -> Self {
Self {
task_id: task_id.into(),
}
}
pub fn task_id(&self) -> &str {
&self.task_id
}
pub async fn get_result_via(
&self,
client: &ZaiClient,
format_type: FormatType,
) -> ZaiResult<FileParseResultResponse> {
if self.task_id.trim().is_empty() {
return Err(crate::ZaiError::ApiError {
code: codes::SDK_VALIDATION,
message: "task_id cannot be blank".to_string(),
});
}
let route = crate::client::routes::FILES_PARSE_RESULT;
let format_type = format_type.to_string();
let url = client
.endpoints()
.resolve_route(route, &[&self.task_id, &format_type])?;
trace!(format = %format_type, "Fetching file parser result");
client
.send_empty::<FileParseResultResponse>(route.method(), url)
.await
}
pub async fn wait_for_result_via(
&self,
client: &ZaiClient,
format_type: FormatType,
timeout_seconds: u64,
poll_interval_seconds: u64,
) -> ZaiResult<FileParseResultResponse> {
if poll_interval_seconds == 0 {
return Err(crate::client::error::ZaiError::ApiError {
code: codes::SDK_VALIDATION,
message: "poll_interval_seconds must be at least 1".to_string(),
});
}
if timeout_seconds == 0 {
return Err(crate::client::validation::invalid(
"timeout_seconds must be at least 1",
));
}
debug!(
timeout_seconds,
poll_interval_seconds, "Polling file parser result"
);
let timeout = tokio::time::Duration::from_secs(timeout_seconds);
let deadline = tokio::time::Instant::now()
.checked_add(timeout)
.ok_or_else(|| crate::client::error::ZaiError::ApiError {
code: codes::SDK_VALIDATION,
message: "timeout_seconds is too large".to_string(),
})?;
loop {
trace!("Checking file parser result status");
let result =
tokio::time::timeout_at(deadline, self.get_result_via(client, format_type))
.await
.map_err(|_| polling_timeout())??;
match result.status {
ParserStatus::Succeeded => {
debug!("File parsing completed successfully");
return Ok(result);
},
ParserStatus::Failed => {
warn!("File parsing task reported failure");
return Err(parsing_failure(&result.message));
},
ParserStatus::Processing => {
let now = tokio::time::Instant::now();
let elapsed = timeout.saturating_sub(deadline.saturating_duration_since(now));
trace!(elapsed = ?elapsed, "File parser result still processing");
if now >= deadline {
warn!(
elapsed_seconds = elapsed.as_secs(),
timeout_seconds, "Polling timed out waiting for parsing result"
);
return Err(polling_timeout());
}
let interval = tokio::time::Duration::from_secs(poll_interval_seconds);
let wake_at = now.checked_add(interval).unwrap_or(deadline).min(deadline);
tokio::time::sleep_until(wake_at).await;
if wake_at == deadline {
return Err(polling_timeout());
}
},
}
}
}
pub async fn get_all_results_via(
&self,
client: &ZaiClient,
) -> ZaiResult<(FileParseResultResponse, FileParseResultResponse)> {
let (text_result, download_result) = tokio::try_join!(
self.get_result_via(client, FormatType::Text),
self.get_result_via(client, FormatType::DownloadLink),
)?;
Ok((text_result, download_result))
}
}
fn polling_timeout() -> crate::client::error::ZaiError {
crate::client::error::ZaiError::ApiError {
code: codes::SDK_TIMEOUT,
message: "Timeout waiting for parsing result".to_string(),
}
}
fn parsing_failure(message: &str) -> crate::client::error::ZaiError {
crate::client::error::ZaiError::ApiError {
code: codes::SDK_EXTERNAL_TOOL,
message: format!(
"Parsing failed: {}",
crate::client::error::mask_sensitive_info(message)
),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn provider_failure_messages_are_sanitized() {
let error = parsing_failure("Authorization: Bearer private-token-value");
let rendered = error.to_string();
assert!(!rendered.contains("private-token-value"));
assert!(rendered.contains("[AUTH_REDACTED]"));
}
}