deeprl 0.5.0

DeepL client library with all the things (blocking)
Documentation
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

use reqwest::blocking::multipart;

use crate::{builder, DeepL, Error, Formality, Language};

/// Document handle
#[derive(Debug, Deserialize, Serialize)]
pub struct Document {
    /// A unique ID assigned to the uploaded document
    pub document_id: String,
    /// Document encryption key
    pub document_key: String,
}

/// Document translation status
#[derive(Debug, Deserialize)]
pub struct DocumentStatus {
    /// A unique ID assigned to the uploaded document
    pub document_id: String,
    /// A short description of the current state of the document translation process
    pub status: DocState,
    /// Estimated number of seconds until the translation is done.
    /// This parameter is only included while status is "translating".
    pub seconds_remaining: Option<u64>,
    /// The number of characters billed to your account
    pub billed_characters: Option<u64>,
    /// Description of the error, if available.
    /// This parameter may be included if an error occurred during translation.
    pub error_message: Option<String>,
}

/// Document state
#[derive(Clone, Copy, Debug, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DocState {
    /// The translation job is waiting in line to be processed
    Queued,
    /// The translation is currently ongoing
    Translating,
    /// The translation is done and the document is ready for download
    Done,
    /// An irrecoverable error occurred while translating the document
    Error,
}

// DocumentOptions builder
builder! {
    Document {
        @must{
            target_lang: Language,
            file_path: PathBuf,
        };
        @optional{
            source_lang: Language,
            filename: String,
            formality: Formality,
            glossary_id: String,
        };
    }
}

impl DocumentStatus {
    /// Whether the document is done translating and ready to be downloaded
    pub fn is_done(&self) -> bool {
        matches!(self.status, DocState::Done)
    }
}

impl DocumentOptions {
    /// Creates a multipart request form from an instance of `DocumentOptions`
    fn into_multipart(self) -> Result<multipart::Form, Error> {
        let mut form = multipart::Form::new()
            .file("file", self.file_path)
            .map_err(|_| Error::Api("failed to attach file".to_string()))?
            .text("target_lang", self.target_lang.to_string());

        if let Some(src) = self.source_lang {
            form = form.text("source_lang", src.to_string());
        }
        if let Some(name) = self.filename {
            form = form.text("filename", name);
        }
        if let Some(formality) = self.formality {
            form = form.text("formality", formality.to_string());
        }
        if let Some(glos) = self.glossary_id {
            form = form.text("glossary_id", glos);
        }

        Ok(form)
    }
}

impl DeepL {
    /// POST /document
    ///
    /// Upload a document.
    ///
    /// Translating a document consists of three steps: upload, polling translation status,
    /// and download. `document_upload` returns a document handle that we need in order to
    /// complete the remaining steps: getting the [`document_status`](Self::document_status)
    /// and finally fetching the translation result with
    /// [`document_download`](Self::document_download).
    ///
    /// ## Example
    ///
    /// ```rust,no_run
    /// # use deeprl::*;
    /// # use std::{env, fs, path::PathBuf, thread};
    /// # let dl = DeepL::new(&env::var("DEEPL_API_KEY").unwrap());
    /// # use std::time::Duration;
    /// let file_path = PathBuf::from("test.txt");
    /// let target_lang = Language::De;
    /// let opt = DocumentOptions::new(target_lang, file_path);
    /// let doc = dl.document_upload(opt).unwrap();
    ///
    /// while !dl.document_status(&doc).unwrap().is_done() {
    ///     // wait a second
    ///     thread::sleep(Duration::from_secs(1));
    /// }
    ///
    /// let out_file = PathBuf::from("test-translated.txt");
    /// let _ = dl.document_download(doc, Some(out_file.clone())).unwrap();
    /// let content = fs::read_to_string(out_file).unwrap();
    /// assert!(!content.is_empty());
    /// ```
    pub fn document_upload(&self, opt: DocumentOptions) -> Result<Document, Error> {
        let url = format!("{}/document", self.url);

        let form = opt.into_multipart()?;

        let resp = self
            .post(url)
            .multipart(form)
            .send()
            .map_err(Error::Reqwest)?;

        if !resp.status().is_success() {
            return Err(Error::Response(
                resp.status(),
                resp.text().unwrap_or_default(),
            ));
        }

        Ok(resp.json()?)
    }

    /// POST /document/`{document_id}`
    ///
    /// Get document translation status. In case there's an issue with translation,
    /// [`DocumentStatus`] contains a field `error_message` that may provide context
    /// for the cause of the error.
    pub fn document_status(&self, doc: &Document) -> Result<DocumentStatus, Error> {
        let doc_id = doc.document_id.clone();
        let url = format!("{}/document/{}", self.url, doc_id);

        let key = doc.document_key.clone();
        let params = vec![("document_key", key)];

        let resp = self
            .post(url)
            .form(&params)
            .send()
            .map_err(Error::Reqwest)?;

        if !resp.status().is_success() {
            return Err(Error::Response(
                resp.status(),
                resp.text().unwrap_or_default(),
            ));
        }

        Ok(resp.json()?)
    }

    /// POST /document/`{document_id}`/result
    ///
    /// Download translated document.
    ///
    /// If no `out_file` is given, the returned file path will have the name of the
    /// [`Document`] id.
    pub fn document_download(
        &self,
        doc: Document,
        out_file: Option<PathBuf>,
    ) -> Result<PathBuf, Error> {
        let doc_id = doc.document_id;
        let url = format!("{}/document/{}/result", self.url, doc_id);

        let params = vec![("document_key", doc.document_key)];

        let mut resp = self
            .post(url)
            .form(&params)
            .send()
            .map_err(Error::Reqwest)?;

        if !resp.status().is_success() {
            return Err(Error::Response(
                resp.status(),
                resp.text().unwrap_or_default(),
            ));
        }

        // write out file
        let mut buf: Vec<u8> = vec![];
        let _ = resp.copy_to(&mut buf).map_err(Error::Reqwest)?;

        let path = out_file.unwrap_or(PathBuf::from(doc_id));
        std::fs::write(&path, buf).map_err(Error::Io)?;

        Ok(path)
    }
}