Skip to main content

zai_rs/tool/file_parser_result/
data.rs

1//! File-parser result request and polling helpers.
2
3use tracing::{debug, trace, warn};
4
5use super::{request::*, response::*};
6use crate::{
7    ZaiResult,
8    client::{ZaiClient, error::codes},
9};
10
11/// Handle for retrieving or polling one file-parsing task.
12///
13/// # Examples
14///
15/// ```rust,no_run
16/// use zai_rs::tool::file_parser_result::{FileParseResultRequest, FormatType};
17/// use zai_rs::ZaiClient;
18///
19/// # async fn fetch(client: &ZaiClient) -> zai_rs::ZaiResult<()> {
20/// let request = FileParseResultRequest::new("task_123456789");
21/// let response = request.get_result_via(client, FormatType::Text).await?;
22/// if let Some(content) = response.content() {
23///     println!("Parsed content: {}", content);
24/// }
25/// # Ok(())
26/// # }
27/// ```
28pub struct FileParseResultRequest {
29    /// Task identifier returned by the creation endpoint.
30    task_id: String,
31}
32
33impl FileParseResultRequest {
34    /// Create a result handle for `task_id`.
35    pub fn new(task_id: impl Into<String>) -> Self {
36        Self {
37            task_id: task_id.into(),
38        }
39    }
40
41    /// Borrow the task identifier used by result requests.
42    pub fn task_id(&self) -> &str {
43        &self.task_id
44    }
45
46    /// Fetch the current task result in `format_type` through `client`.
47    pub async fn get_result_via(
48        &self,
49        client: &ZaiClient,
50        format_type: FormatType,
51    ) -> ZaiResult<FileParseResultResponse> {
52        if self.task_id.trim().is_empty() {
53            return Err(crate::ZaiError::ApiError {
54                code: codes::SDK_VALIDATION,
55                message: "task_id cannot be blank".to_string(),
56            });
57        }
58        let route = crate::client::routes::FILES_PARSE_RESULT;
59        let format_type = format_type.to_string();
60        let url = client
61            .endpoints()
62            .resolve_route(route, &[&self.task_id, &format_type])?;
63        trace!(format = %format_type, "Fetching file parser result");
64        client
65            .send_empty::<FileParseResultResponse>(route.method(), url)
66            .await
67    }
68
69    /// Poll until the task succeeds, fails, or reaches `timeout_seconds`.
70    ///
71    /// `poll_interval_seconds` must be at least one. A provider failure and a
72    /// local timeout are returned as errors rather than successful responses.
73    pub async fn wait_for_result_via(
74        &self,
75        client: &ZaiClient,
76        format_type: FormatType,
77        timeout_seconds: u64,
78        poll_interval_seconds: u64,
79    ) -> ZaiResult<FileParseResultResponse> {
80        if poll_interval_seconds == 0 {
81            return Err(crate::client::error::ZaiError::ApiError {
82                code: codes::SDK_VALIDATION,
83                message: "poll_interval_seconds must be at least 1".to_string(),
84            });
85        }
86        if timeout_seconds == 0 {
87            return Err(crate::client::validation::invalid(
88                "timeout_seconds must be at least 1",
89            ));
90        }
91        debug!(
92            timeout_seconds,
93            poll_interval_seconds, "Polling file parser result"
94        );
95        let timeout = tokio::time::Duration::from_secs(timeout_seconds);
96        let deadline = tokio::time::Instant::now()
97            .checked_add(timeout)
98            .ok_or_else(|| crate::client::error::ZaiError::ApiError {
99                code: codes::SDK_VALIDATION,
100                message: "timeout_seconds is too large".to_string(),
101            })?;
102
103        loop {
104            trace!("Checking file parser result status");
105            let result =
106                tokio::time::timeout_at(deadline, self.get_result_via(client, format_type))
107                    .await
108                    .map_err(|_| polling_timeout())??;
109
110            match result.status {
111                ParserStatus::Succeeded => {
112                    debug!("File parsing completed successfully");
113                    return Ok(result);
114                },
115                ParserStatus::Failed => {
116                    warn!("File parsing task reported failure");
117                    return Err(parsing_failure(&result.message));
118                },
119                ParserStatus::Processing => {
120                    let now = tokio::time::Instant::now();
121                    let elapsed = timeout.saturating_sub(deadline.saturating_duration_since(now));
122                    trace!(elapsed = ?elapsed, "File parser result still processing");
123                    if now >= deadline {
124                        warn!(
125                            elapsed_seconds = elapsed.as_secs(),
126                            timeout_seconds, "Polling timed out waiting for parsing result"
127                        );
128                        return Err(polling_timeout());
129                    }
130                    let interval = tokio::time::Duration::from_secs(poll_interval_seconds);
131                    let wake_at = now.checked_add(interval).unwrap_or(deadline).min(deadline);
132                    tokio::time::sleep_until(wake_at).await;
133                    if wake_at == deadline {
134                        return Err(polling_timeout());
135                    }
136                },
137            }
138        }
139    }
140
141    /// Fetch both text and download-link representations.
142    ///
143    /// This performs the two independent HTTP requests concurrently.
144    ///
145    pub async fn get_all_results_via(
146        &self,
147        client: &ZaiClient,
148    ) -> ZaiResult<(FileParseResultResponse, FileParseResultResponse)> {
149        let (text_result, download_result) = tokio::try_join!(
150            self.get_result_via(client, FormatType::Text),
151            self.get_result_via(client, FormatType::DownloadLink),
152        )?;
153        Ok((text_result, download_result))
154    }
155}
156
157fn polling_timeout() -> crate::client::error::ZaiError {
158    crate::client::error::ZaiError::ApiError {
159        code: codes::SDK_TIMEOUT,
160        message: "Timeout waiting for parsing result".to_string(),
161    }
162}
163
164fn parsing_failure(message: &str) -> crate::client::error::ZaiError {
165    crate::client::error::ZaiError::ApiError {
166        code: codes::SDK_EXTERNAL_TOOL,
167        message: format!(
168            "Parsing failed: {}",
169            crate::client::error::mask_sensitive_info(message)
170        ),
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn provider_failure_messages_are_sanitized() {
180        let error = parsing_failure("Authorization: Bearer private-token-value");
181        let rendered = error.to_string();
182        assert!(!rendered.contains("private-token-value"));
183        assert!(rendered.contains("[AUTH_REDACTED]"));
184    }
185}