Skip to main content

cloudiful_docling_convert/api/
result.rs

1use bytes::Bytes;
2use reqwest::Response;
3use serde_json::Value;
4
5use crate::error::{PdfConvertError, Result};
6use crate::models::{
7    ChunkDocumentResponse, ConvertDocumentResponse, DoclingErrorItem, TaskFailureResult,
8};
9
10use super::transport::handle_response;
11
12#[derive(Debug, Clone)]
13pub enum DoclingResult {
14    Convert(ConvertDocumentResponse),
15    Chunk(ChunkDocumentResponse),
16    Failure(TaskFailureResult),
17    Json(Value),
18    Zip(Bytes),
19}
20
21#[derive(Debug, Clone)]
22pub struct DoclingTaskResult {
23    pub status: crate::models::ConversionStatus,
24    pub result: DoclingResult,
25    pub errors: Vec<String>,
26}
27
28impl DoclingResult {
29    pub fn errors(&self) -> Vec<String> {
30        match self {
31            Self::Convert(response) => response.errors.iter().map(format_error).collect(),
32            Self::Chunk(response) => response
33                .documents
34                .iter()
35                .flat_map(|document| document.errors.iter())
36                .map(format_error)
37                .collect(),
38            Self::Failure(response) => vec![response.failure.message.clone()],
39            Self::Json(value) => collect_json_errors(value),
40            Self::Zip(_) => Vec::new(),
41        }
42    }
43
44    pub fn filename(&self) -> Option<&str> {
45        match self {
46            Self::Convert(response) => Some(&response.document.filename),
47            Self::Chunk(response) => response
48                .chunks
49                .first()
50                .map(|chunk| chunk.filename.as_str())
51                .or_else(|| {
52                    response
53                        .documents
54                        .first()
55                        .map(|document| document.document.filename.as_str())
56                }),
57            Self::Failure(_) | Self::Json(_) | Self::Zip(_) => None,
58        }
59    }
60
61    pub fn into_json(self) -> Option<Value> {
62        match self {
63            Self::Convert(response) => serde_json::to_value(response).ok(),
64            Self::Chunk(response) => serde_json::to_value(response).ok(),
65            Self::Failure(response) => serde_json::to_value(response).ok(),
66            Self::Json(value) => Some(value),
67            Self::Zip(_) => None,
68        }
69    }
70}
71
72pub(crate) async fn parse_response(response: Response, context: &str) -> Result<DoclingResult> {
73    let response = handle_response(response, context).await?;
74    let is_zip = response
75        .headers()
76        .get(reqwest::header::CONTENT_TYPE)
77        .and_then(|value| value.to_str().ok())
78        .is_some_and(|value| value.to_ascii_lowercase().contains("application/zip"));
79    let bytes = response.bytes().await.map_err(PdfConvertError::from)?;
80    parse_bytes(bytes, is_zip)
81}
82
83pub(crate) fn parse_bytes(bytes: Bytes, is_zip: bool) -> Result<DoclingResult> {
84    if is_zip || bytes.as_ref().starts_with(b"PK\x03\x04") {
85        return Ok(DoclingResult::Zip(bytes));
86    }
87
88    let value: Value = serde_json::from_slice(&bytes).map_err(|error| {
89        PdfConvertError::parse_error("Docling result response", error.to_string())
90    })?;
91    parse_json(value)
92}
93
94pub(crate) fn parse_json(value: Value) -> Result<DoclingResult> {
95    if value
96        .get("kind")
97        .and_then(Value::as_str)
98        .is_some_and(|kind| kind == "TaskFailureResult")
99        || value.get("failure").is_some()
100    {
101        return serde_json::from_value(value)
102            .map(DoclingResult::Failure)
103            .map_err(|error| {
104                PdfConvertError::parse_error("Docling failure result", error.to_string())
105            });
106    }
107
108    if value.get("chunks").is_some() {
109        return serde_json::from_value(value)
110            .map(DoclingResult::Chunk)
111            .map_err(|error| {
112                PdfConvertError::parse_error("Docling chunk result", error.to_string())
113            });
114    }
115
116    if value.get("document").is_some() && value.get("status").is_some() {
117        return serde_json::from_value(value)
118            .map(DoclingResult::Convert)
119            .map_err(|error| {
120                PdfConvertError::parse_error("Docling conversion result", error.to_string())
121            });
122    }
123
124    Ok(DoclingResult::Json(value))
125}
126
127fn format_error(error: &DoclingErrorItem) -> String {
128    let message = error
129        .error_message
130        .as_deref()
131        .unwrap_or("unknown document error");
132    match error.page_no {
133        Some(page) => format!("page {page}: {message}"),
134        None => message.to_string(),
135    }
136}
137
138fn collect_json_errors(value: &Value) -> Vec<String> {
139    let mut errors = Vec::new();
140    for key in ["error_message", "message"] {
141        if let Some(message) = value.get(key).and_then(Value::as_str) {
142            errors.push(message.to_string());
143        }
144    }
145    if let Some(message) = value
146        .get("failure")
147        .and_then(|failure| failure.get("message"))
148        .and_then(Value::as_str)
149    {
150        errors.push(message.to_string());
151    }
152    if let Some(message) = value.get("detail").and_then(Value::as_str) {
153        errors.push(message.to_string());
154    }
155    if let Some(items) = value.get("errors").and_then(Value::as_array) {
156        errors.extend(items.iter().filter_map(|item| {
157            item.get("error_message")
158                .or_else(|| item.get("message"))
159                .and_then(Value::as_str)
160                .map(ToString::to_string)
161        }));
162    }
163    errors
164}