Skip to main content

cloudiful_docling_convert/
facade.rs

1use bytes::Bytes;
2
3use crate::api::DoclingConfig;
4use crate::conversion::{
5    ConversionBehavior, DoclingRuntimeConfig, build_convert_options, build_docling_client,
6};
7use crate::document::{ConvertRequest, ConvertedDocument, InputDocument, InputKind, OutputFormat};
8use crate::error::{PdfConvertError, Result};
9use crate::processor::DocumentConverter;
10
11pub struct ConverterBuilder {
12    config: DoclingRuntimeConfig,
13    behavior: ConversionBehavior,
14    output_formats: Vec<OutputFormat>,
15}
16
17impl ConverterBuilder {
18    pub fn new(config: DoclingRuntimeConfig) -> Self {
19        Self {
20            config,
21            behavior: ConversionBehavior::default(),
22            output_formats: vec![OutputFormat::Md],
23        }
24    }
25
26    pub fn behavior(mut self, behavior: ConversionBehavior) -> Self {
27        self.behavior = behavior;
28        self
29    }
30
31    pub fn output_formats(mut self, output_formats: Vec<OutputFormat>) -> Self {
32        self.output_formats = output_formats;
33        self
34    }
35
36    pub fn build(self) -> Result<PdfConvert> {
37        let output_formats = if self.output_formats.is_empty() {
38            vec![OutputFormat::Md]
39        } else {
40            self.output_formats
41        };
42
43        Ok(PdfConvert {
44            converter: DocumentConverter::new(build_docling_client(self.config)?),
45            behavior: self.behavior,
46            output_formats,
47        })
48    }
49}
50
51pub struct PdfConvert {
52    converter: DocumentConverter,
53    behavior: ConversionBehavior,
54    output_formats: Vec<OutputFormat>,
55}
56
57impl PdfConvert {
58    pub fn builder(config: DoclingRuntimeConfig) -> ConverterBuilder {
59        ConverterBuilder::new(config)
60    }
61
62    pub fn from_runtime_config(config: DoclingRuntimeConfig) -> Result<Self> {
63        Self::builder(config).build()
64    }
65
66    pub fn from_docling_config(config: DoclingConfig) -> Result<Self> {
67        Ok(Self {
68            converter: DocumentConverter::new(crate::DoclingClient::new(config)?),
69            behavior: ConversionBehavior::default(),
70            output_formats: vec![OutputFormat::Md],
71        })
72    }
73
74    pub fn request_for_input(&self, input: InputDocument) -> Result<ConvertRequest> {
75        let input_kind = input.kind()?;
76
77        Ok(ConvertRequest {
78            input,
79            output_formats: self.output_formats.clone(),
80            options: build_convert_options(input_kind, &self.behavior)?,
81        })
82    }
83
84    pub async fn convert_input(&self, input: InputDocument) -> Result<ConvertedDocument> {
85        self.converter.convert(self.request_for_input(input)?).await
86    }
87
88    pub async fn convert_bytes(
89        &self,
90        filename: impl Into<String>,
91        bytes: impl Into<Bytes>,
92    ) -> Result<ConvertedDocument> {
93        let filename = filename.into();
94        let input_kind =
95            InputKind::from_filename_and_media_type(&filename, None).ok_or_else(|| {
96                PdfConvertError::validation_error(
97                    "filename",
98                    format!("unsupported input type for '{}'", filename),
99                )
100            })?;
101
102        self.convert_input(InputDocument::new(
103            filename.clone(),
104            input_kind.canonical_media_type(&filename, None),
105            bytes,
106        ))
107        .await
108    }
109
110    pub async fn convert_bytes_with_input_kind(
111        &self,
112        filename: impl Into<String>,
113        bytes: impl Into<Bytes>,
114        input_kind: InputKind,
115    ) -> Result<ConvertedDocument> {
116        let filename = filename.into();
117
118        self.convert_input(
119            InputDocument::new(
120                filename.clone(),
121                input_kind.canonical_media_type(&filename, None),
122                bytes,
123            )
124            .with_input_kind(input_kind),
125        )
126        .await
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn builder_defaults_to_markdown_output() {
136        let converter = ConverterBuilder::new(DoclingRuntimeConfig {
137            docling_base_url: "http://127.0.0.1:5001/v1".into(),
138            openai_base_url: "https://example.com/v1".into(),
139            vlm_pipeline_model: "test-model".into(),
140            picture_description_model: "test-model".into(),
141            code_formula_model: "test-model".into(),
142            api_key: Some("key".into()),
143        })
144        .build()
145        .unwrap();
146
147        let request = converter
148            .request_for_input(InputDocument::new(
149                "notes.md",
150                "text/markdown",
151                Bytes::from("# hi"),
152            ))
153            .unwrap();
154
155        assert_eq!(request.output_formats, vec![OutputFormat::Md]);
156    }
157
158    #[test]
159    fn convert_bytes_rejects_unknown_extensions() {
160        let converter = ConverterBuilder::new(DoclingRuntimeConfig {
161            docling_base_url: "http://127.0.0.1:5001/v1".into(),
162            openai_base_url: "https://example.com/v1".into(),
163            vlm_pipeline_model: "test-model".into(),
164            picture_description_model: "test-model".into(),
165            code_formula_model: "test-model".into(),
166            api_key: Some("key".into()),
167        })
168        .build()
169        .unwrap();
170
171        let error = tokio::runtime::Runtime::new()
172            .unwrap()
173            .block_on(converter.convert_bytes("notes.bin", Bytes::from_static(b"test")))
174            .unwrap_err();
175
176        assert!(error.to_string().contains("unsupported input type"));
177    }
178
179    #[test]
180    fn convert_bytes_with_input_kind_accepts_ambiguous_sources() {
181        let converter = ConverterBuilder::new(DoclingRuntimeConfig {
182            docling_base_url: "http://127.0.0.1:5001/v1".into(),
183            openai_base_url: "https://example.com/v1".into(),
184            vlm_pipeline_model: "test-model".into(),
185            picture_description_model: "test-model".into(),
186            code_formula_model: "test-model".into(),
187            api_key: Some("key".into()),
188        })
189        .build()
190        .unwrap();
191
192        let request = converter
193            .request_for_input(
194                InputDocument::new("paper.xml", "application/xml", Bytes::from("<article />"))
195                    .with_input_kind(InputKind::XmlJats),
196            )
197            .unwrap();
198
199        assert_eq!(request.input.kind().unwrap(), InputKind::XmlJats);
200    }
201}