Skip to main content

zai_rs/file/
parse_sync.rs

1//! Synchronous multipart file parsing.
2
3use std::path::{Path, PathBuf};
4
5use serde::{Deserialize, Serialize};
6
7use crate::{ZaiResult, client::ZaiClient};
8
9/// Response returned by synchronous parsing. The sync and result-retrieval
10/// endpoints share the frozen `FileParseResultResponse` schema.
11pub type FileResponse = crate::tool::FileParseResultResponse;
12
13/// Optional file type accepted by the synchronous `prime-sync` parser.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
15#[serde(rename_all = "UPPERCASE")]
16pub enum FileParseSyncFileType {
17    /// WPS document.
18    WPS,
19    /// PDF document.
20    PDF,
21    /// Office Open XML Word document.
22    DOCX,
23    /// Legacy Word document.
24    DOC,
25    /// Legacy Excel workbook.
26    XLS,
27    /// Office Open XML Excel workbook.
28    XLSX,
29    /// Legacy PowerPoint presentation.
30    PPT,
31    /// Office Open XML PowerPoint presentation.
32    PPTX,
33    /// PNG image.
34    PNG,
35    /// JPG image.
36    JPG,
37    /// JPEG image.
38    JPEG,
39    /// Comma-separated values file.
40    CSV,
41    /// Plain-text file.
42    TXT,
43    /// Markdown file.
44    MD,
45    /// HTML file.
46    HTML,
47    /// Bitmap image.
48    BMP,
49    /// GIF image.
50    GIF,
51    /// WebP image.
52    WEBP,
53    /// HEIC image.
54    HEIC,
55    /// Encapsulated PostScript file.
56    EPS,
57    /// Apple icon file.
58    ICNS,
59    /// ImageMagick image.
60    IM,
61    /// PCX image.
62    PCX,
63    /// Portable pixmap image.
64    PPM,
65    /// TIFF image.
66    TIFF,
67    /// X bitmap image.
68    XBM,
69    /// HEIF image.
70    HEIF,
71    /// JPEG 2000 image.
72    JP2,
73}
74
75impl FileParseSyncFileType {
76    /// Return the exact uppercase multipart value.
77    pub const fn as_str(self) -> &'static str {
78        match self {
79            Self::WPS => "WPS",
80            Self::PDF => "PDF",
81            Self::DOCX => "DOCX",
82            Self::DOC => "DOC",
83            Self::XLS => "XLS",
84            Self::XLSX => "XLSX",
85            Self::PPT => "PPT",
86            Self::PPTX => "PPTX",
87            Self::PNG => "PNG",
88            Self::JPG => "JPG",
89            Self::JPEG => "JPEG",
90            Self::CSV => "CSV",
91            Self::TXT => "TXT",
92            Self::MD => "MD",
93            Self::HTML => "HTML",
94            Self::BMP => "BMP",
95            Self::GIF => "GIF",
96            Self::WEBP => "WEBP",
97            Self::HEIC => "HEIC",
98            Self::EPS => "EPS",
99            Self::ICNS => "ICNS",
100            Self::IM => "IM",
101            Self::PCX => "PCX",
102            Self::PPM => "PPM",
103            Self::TIFF => "TIFF",
104            Self::XBM => "XBM",
105            Self::HEIF => "HEIF",
106            Self::JP2 => "JP2",
107        }
108    }
109}
110
111/// Synchronous file-parsing request for `POST /files/parser/sync`.
112///
113/// The parser implementation is fixed to the required wire value
114/// `prime-sync`; callers select only the local file and optional file type.
115pub struct FileParseSyncRequest {
116    file_path: PathBuf,
117    file_type: Option<FileParseSyncFileType>,
118}
119
120impl std::fmt::Debug for FileParseSyncRequest {
121    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122        formatter
123            .debug_struct("FileParseSyncRequest")
124            .field("file_path", &"[REDACTED]")
125            .field("file_type", &self.file_type)
126            .finish()
127    }
128}
129
130impl FileParseSyncRequest {
131    /// Create a synchronous parsing request for a local file.
132    pub fn new(file_path: impl Into<PathBuf>) -> Self {
133        Self {
134            file_path: file_path.into(),
135            file_type: None,
136        }
137    }
138
139    /// Set the optional parser file type.
140    pub fn with_file_type(mut self, file_type: FileParseSyncFileType) -> Self {
141        self.file_type = Some(file_type);
142        self
143    }
144
145    /// Borrow the local file path.
146    pub fn file_path(&self) -> &Path {
147        &self.file_path
148    }
149
150    /// Return the fixed parser implementation.
151    pub const fn tool_type(&self) -> &'static str {
152        "prime-sync"
153    }
154
155    /// Return the optional declared file type.
156    pub const fn file_type(&self) -> Option<FileParseSyncFileType> {
157        self.file_type
158    }
159
160    /// Validate the local file, stream it as multipart, and decode the parsing
161    /// result. The file is reopened and revalidated for the transport attempt.
162    pub async fn send_via(&self, client: &ZaiClient) -> ZaiResult<FileResponse> {
163        let file_part = crate::client::transport::multipart::FilePart::from_path(&self.file_path)?;
164        let route = crate::client::routes::FILES_PARSE_SYNC;
165        let url = client.endpoints().resolve_route(route, &[])?;
166        let mut factory = crate::client::transport::multipart::MultipartBodyFactory::new()
167            .field("tool_type", self.tool_type())?;
168        if let Some(file_type) = self.file_type {
169            factory = factory.field("file_type", file_type.as_str())?;
170        }
171        factory = factory.file_named("file", file_part)?;
172
173        client
174            .send_multipart::<FileResponse>(route.method(), url, &factory)
175            .await
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn file_type_values_match_the_frozen_enum() {
185        let values = [
186            FileParseSyncFileType::WPS,
187            FileParseSyncFileType::PDF,
188            FileParseSyncFileType::DOCX,
189            FileParseSyncFileType::DOC,
190            FileParseSyncFileType::XLS,
191            FileParseSyncFileType::XLSX,
192            FileParseSyncFileType::PPT,
193            FileParseSyncFileType::PPTX,
194            FileParseSyncFileType::PNG,
195            FileParseSyncFileType::JPG,
196            FileParseSyncFileType::JPEG,
197            FileParseSyncFileType::CSV,
198            FileParseSyncFileType::TXT,
199            FileParseSyncFileType::MD,
200            FileParseSyncFileType::HTML,
201            FileParseSyncFileType::BMP,
202            FileParseSyncFileType::GIF,
203            FileParseSyncFileType::WEBP,
204            FileParseSyncFileType::HEIC,
205            FileParseSyncFileType::EPS,
206            FileParseSyncFileType::ICNS,
207            FileParseSyncFileType::IM,
208            FileParseSyncFileType::PCX,
209            FileParseSyncFileType::PPM,
210            FileParseSyncFileType::TIFF,
211            FileParseSyncFileType::XBM,
212            FileParseSyncFileType::HEIF,
213            FileParseSyncFileType::JP2,
214        ];
215        let expected = [
216            "WPS", "PDF", "DOCX", "DOC", "XLS", "XLSX", "PPT", "PPTX", "PNG", "JPG", "JPEG", "CSV",
217            "TXT", "MD", "HTML", "BMP", "GIF", "WEBP", "HEIC", "EPS", "ICNS", "IM", "PCX", "PPM",
218            "TIFF", "XBM", "HEIF", "JP2",
219        ];
220        assert_eq!(values.map(FileParseSyncFileType::as_str), expected);
221        for value in values {
222            assert_eq!(
223                serde_json::to_value(value).unwrap(),
224                serde_json::Value::String(value.as_str().to_owned())
225            );
226        }
227    }
228
229    #[test]
230    fn response_required_fields_do_not_default() {
231        assert!(
232            serde_json::from_value::<FileResponse>(serde_json::json!({
233                "status": "succeeded",
234                "message": "ok"
235            }))
236            .is_err()
237        );
238    }
239}