cloudiful-docling-convert 0.3.2

Core Rust library for Docling-backed document conversion
Documentation
use std::collections::BTreeMap;

use serde::Serialize;
use serde_json::Value;

use crate::document::{ChunkerKind, ChunkingOptions, InputKind, PipelineKind};

use super::docling::DoclingConvertRequest;

#[derive(Debug, Clone, Serialize)]
#[serde(tag = "kind")]
pub enum HttpSource {
    #[serde(rename = "http")]
    Http {
        url: String,
        #[serde(default)]
        headers: BTreeMap<String, Value>,
    },
}

impl HttpSource {
    pub fn new(url: impl Into<String>) -> Self {
        Self::Http {
            url: url.into(),
            headers: BTreeMap::new(),
        }
    }
}

#[derive(Debug, Clone, Serialize)]
#[serde(tag = "kind")]
pub enum TargetKind {
    #[serde(rename = "inbody")]
    Inbody {},
    #[serde(rename = "zip")]
    Zip {},
}

#[derive(Debug, Clone, Serialize)]
pub(crate) struct ConvertDocumentsOptions {
    pub from_formats: Vec<String>,
    pub to_formats: Vec<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub pipeline: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub page_range: Option<[u64; 2]>,
}

#[derive(Debug, Clone, Serialize)]
pub(crate) struct ConvertSourcesRequest {
    pub options: ConvertDocumentsOptions,
    pub sources: Vec<HttpSource>,
    pub target: TargetKind,
}

#[derive(Debug, Clone, Serialize)]
pub(crate) struct ChunkSourcesRequest {
    pub convert_options: ConvertDocumentsOptions,
    pub sources: Vec<HttpSource>,
    pub include_converted_doc: bool,
    pub target: TargetKind,
    pub chunking_options: ChunkingRequestOptions,
}

#[derive(Debug, Clone, Serialize)]
#[serde(tag = "chunker")]
pub(crate) enum ChunkingRequestOptions {
    #[serde(rename = "hybrid")]
    Hybrid {
        use_markdown_tables: bool,
        use_markdown_images: bool,
        image_placeholder: String,
        include_raw_text: bool,
        max_tokens: Option<u32>,
        tokenizer: String,
        merge_peers: bool,
    },
    #[serde(rename = "hierarchical")]
    Hierarchical {
        use_markdown_tables: bool,
        use_markdown_images: bool,
        image_placeholder: String,
        include_raw_text: bool,
    },
}

pub(crate) fn convert_options(
    input_kind: InputKind,
    request: &DoclingConvertRequest,
) -> ConvertDocumentsOptions {
    ConvertDocumentsOptions {
        from_formats: vec![input_kind.from_formats_value().to_string()],
        to_formats: request
            .output_formats
            .iter()
            .map(|format| format.as_api_value().to_string())
            .collect(),
        pipeline: request.pipeline.map(|pipeline| pipeline.to_string()),
        page_range: request
            .page_range
            .map(|(start, end)| [start as u64, end as u64]),
    }
}

pub(crate) fn target_for(request: &DoclingConvertRequest) -> TargetKind {
    if request
        .output_formats
        .iter()
        .any(|format| format.is_archive())
    {
        TargetKind::Zip {}
    } else {
        TargetKind::Inbody {}
    }
}

pub(crate) fn chunking_options(
    chunker: ChunkerKind,
    options: &ChunkingOptions,
) -> crate::error::Result<ChunkingRequestOptions> {
    match chunker {
        ChunkerKind::Hybrid => {
            let defaults = ChunkingOptions::hybrid_defaults();
            Ok(ChunkingRequestOptions::Hybrid {
                use_markdown_tables: options.use_markdown_tables,
                use_markdown_images: options.use_markdown_images,
                image_placeholder: if options.image_placeholder.is_empty() {
                    defaults.image_placeholder
                } else {
                    options.image_placeholder.clone()
                },
                include_raw_text: options.include_raw_text,
                max_tokens: options.max_tokens,
                tokenizer: options
                    .tokenizer
                    .clone()
                    .unwrap_or_else(|| defaults.tokenizer.unwrap_or_default()),
                merge_peers: options.merge_peers,
            })
        }
        ChunkerKind::Hierarchical => Ok(ChunkingRequestOptions::Hierarchical {
            use_markdown_tables: options.use_markdown_tables,
            use_markdown_images: options.use_markdown_images,
            image_placeholder: if options.image_placeholder.is_empty() {
                ChunkingOptions::hierarchical_defaults().image_placeholder
            } else {
                options.image_placeholder.clone()
            },
            include_raw_text: options.include_raw_text,
        }),
        ChunkerKind::None => Err(crate::error::PdfConvertError::validation_error(
            "chunker",
            "chunking options require hybrid or hierarchical",
        )),
    }
}

pub(crate) fn source_request(
    url: impl Into<String>,
    input_kind: InputKind,
    request: &DoclingConvertRequest,
) -> ConvertSourcesRequest {
    ConvertSourcesRequest {
        options: convert_options(input_kind, request),
        sources: vec![HttpSource::new(url)],
        target: target_for(request),
    }
}

pub(crate) fn chunk_source_request(
    url: impl Into<String>,
    input_kind: InputKind,
    request: &DoclingConvertRequest,
) -> crate::error::Result<ChunkSourcesRequest> {
    Ok(ChunkSourcesRequest {
        convert_options: convert_options(input_kind, request),
        sources: vec![HttpSource::new(url)],
        include_converted_doc: false,
        target: target_for(request),
        chunking_options: chunking_options(request.chunker, &request.chunking)?,
    })
}

impl std::fmt::Display for PipelineKind {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let value = match self {
            PipelineKind::Legacy => "legacy",
            PipelineKind::Standard => "standard",
            PipelineKind::Vlm => "vlm",
            PipelineKind::Asr => "asr",
        };
        f.write_str(value)
    }
}