cloudiful_docling_convert/
facade.rs1use 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 ..DoclingRuntimeConfig::without_vlm("http://127.0.0.1:5001/v1")
144 })
145 .build()
146 .unwrap();
147
148 let request = converter
149 .request_for_input(InputDocument::new(
150 "notes.md",
151 "text/markdown",
152 Bytes::from("# hi"),
153 ))
154 .unwrap();
155
156 assert_eq!(request.output_formats, vec![OutputFormat::Md]);
157 }
158
159 #[test]
160 fn convert_bytes_rejects_unknown_extensions() {
161 let converter = ConverterBuilder::new(DoclingRuntimeConfig {
162 docling_base_url: "http://127.0.0.1:5001/v1".into(),
163 openai_base_url: "https://example.com/v1".into(),
164 vlm_pipeline_model: "test-model".into(),
165 picture_description_model: "test-model".into(),
166 code_formula_model: "test-model".into(),
167 api_key: Some("key".into()),
168 ..DoclingRuntimeConfig::without_vlm("http://127.0.0.1:5001/v1")
169 })
170 .build()
171 .unwrap();
172
173 let error = tokio::runtime::Runtime::new()
174 .unwrap()
175 .block_on(converter.convert_bytes("notes.bin", Bytes::from_static(b"test")))
176 .unwrap_err();
177
178 assert!(error.to_string().contains("unsupported input type"));
179 }
180
181 #[test]
182 fn convert_bytes_with_input_kind_accepts_ambiguous_sources() {
183 let converter = ConverterBuilder::new(DoclingRuntimeConfig {
184 docling_base_url: "http://127.0.0.1:5001/v1".into(),
185 openai_base_url: "https://example.com/v1".into(),
186 vlm_pipeline_model: "test-model".into(),
187 picture_description_model: "test-model".into(),
188 code_formula_model: "test-model".into(),
189 api_key: Some("key".into()),
190 ..DoclingRuntimeConfig::without_vlm("http://127.0.0.1:5001/v1")
191 })
192 .build()
193 .unwrap();
194
195 let request = converter
196 .request_for_input(
197 InputDocument::new("paper.xml", "application/xml", Bytes::from("<article />"))
198 .with_input_kind(InputKind::XmlJats),
199 )
200 .unwrap();
201
202 assert_eq!(request.input.kind().unwrap(), InputKind::XmlJats);
203 }
204}