deeprl 0.5.0

DeepL client library with all the things (blocking)
Documentation
//! # deeprl
//!
//! Access the DeepL translation engine through a quick and reliable interface. We aim to provide the full suite of tools DeepL offers.
//! See the [official docs](https://www.deepl.com/en/docs-api) for detailed resources.
//!
//! ## Note
//! This crate uses a blocking http client, and as such is only suitable for use in synchronous (blocking) applications.
//! If you intend to use the library functions in an async app, there is a [crate](https://docs.rs/deepl/latest/deepl/) for that.
//!  
//! ## Examples
//! Create a new client with a valid API token to access the associated methods. For instance, you may wish to translate a simple text string to some target language.
//! ```
//! use deeprl::{DeepL, Language, TextOptions};
//!
//! let key = std::env::var("DEEPL_API_KEY").unwrap();
//! let dl = DeepL::new(&key);
//!
//! // Translate 'good morning' to German
//! let text = vec![
//!     "good morning".to_string(),
//! ];
//!
//! let opt = TextOptions::new(Language::De).text(text);
//!
//! let result = dl.translate(opt).unwrap();
//! assert!(!result.translations.is_empty());
//!
//! let translation = &result.translations[0];
//! assert_eq!(translation.text, "Guten Morgen");
//! ```
//!
//! As a helpful sanity check, make sure you're able to return account usage statistics.
//! ```
//! use deeprl::DeepL;
//!
//! let dl = DeepL::new(
//!     &std::env::var("DEEPL_API_KEY").unwrap()
//! );
//!
//! let usage = dl.usage().unwrap();
//! assert!(usage.character_limit > 0);
//!
//! let count = usage.character_count;
//! let limit = usage.character_limit;
//! println!("Used: {count}/{limit}");
//! // Used: 42/500000
//! ```
//!
//! [`DeepL`] also allows translating documents and creating custom glossaries.
//!
//! # License
//! This project is licenced under MIT license.

#![warn(missing_docs)]

use core::fmt;
use serde::Deserialize;
use std::io;

use reqwest::header;
use reqwest::StatusCode;

mod doc;
mod glos;
mod lang;
mod text;

// Re-exports
pub use {
    doc::{DocState, Document, DocumentOptions, DocumentStatus},
    glos::{
        GlossariesResult, Glossary, GlossaryEntriesFormat, GlossaryLanguagePair,
        GlossaryLanguagePairsResult,
    },
    lang::{Language, LanguageInfo, LanguageType, ParseLanguageError},
    text::{Formality, SplitSentences, TagHandling, TextOptions, TranslateTextResult, Translation},
};

// Sets the user agent request header value, e.g. 'deeprl/0.1.0'
static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);

/// The `DeepL` client struct
pub struct DeepL {
    client: reqwest::blocking::Client,
    url: reqwest::Url,
    user_agent: Option<String>,
    auth: String,
}

/// Crate error variants
#[derive(Debug)]
pub enum Error {
    /// General API related error
    Api(String),
    /// `std::io` error
    Io(io::Error),
    /// Invalid language
    InvalidLanguage(ParseLanguageError),
    /// Invalid response
    InvalidResponse,
    /// `reqwest` error
    Reqwest(reqwest::Error),
    /// Error sent in the response
    Response(StatusCode, String),
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Api(err) => write!(f, "{err}"),
            Self::Io(err) => write!(f, "{err}"),
            Self::InvalidLanguage(err) => write!(f, "{err}"),
            Self::InvalidResponse => write!(f, "invalid response"),
            Self::Reqwest(err) => write!(f, "{err}"),
            Self::Response(status, err) => write!(f, "{status} {err}"),
        }
    }
}

impl std::error::Error for Error {}

impl From<ParseLanguageError> for Error {
    fn from(err: ParseLanguageError) -> Self {
        Self::InvalidLanguage(err)
    }
}

impl From<reqwest::Error> for Error {
    fn from(err: reqwest::Error) -> Self {
        Self::Reqwest(err)
    }
}

/// API usage & account limits. Currently assumes an individual developer account.
#[derive(Debug, Deserialize)]
pub struct Usage {
    /// Characters translated so far in the current billing period
    pub character_count: u64,
    /// Current maximum number of characters that can be translated per billing period
    pub character_limit: u64,
}

/// Self-implementing type builder
#[macro_export]
macro_rules! builder {
    (
        $name:ident {
            @must{
                $($must_field:ident: $must_type:ty,)+
            };
            @optional{
                $($opt_field:ident: $opt_type:ty,)+
            };
        }
    ) => {
        use paste::paste;

        paste! {
            #[doc = "Options for `" [<$name>] "` translation"]
            #[derive(Debug, Clone, serde::Serialize)]
            pub struct [<$name Options>] {
                $($must_field: $must_type,)+
                $($opt_field: Option<$opt_type>,)+
            }

            impl [<$name Options>] {
                #[must_use]
                #[doc = "Construct a new `" [<$name Options>] "`"]
                pub fn new($($must_field: $must_type,)+) -> Self {
                    Self {
                        $($must_field,)+
                        $($opt_field: None,)+
                    }
                }
                $(
                    #[doc = "Setter for `" $opt_field "`"]
                    pub fn $opt_field(mut self, $opt_field: $opt_type) -> Self {
                        self.$opt_field = Some($opt_field);
                        self
                    }
                )+
            }
        }
    };
}

impl DeepL {
    /// Create a new instance of `DeepL` from an API key.
    pub fn new(key: &str) -> Self {
        let base = if key.ends_with(":fx") {
            "https://api-free.deepl.com/v2"
        } else {
            "https://api.deepl.com/v2"
        };

        DeepL {
            client: reqwest::blocking::Client::new(),
            url: reqwest::Url::parse(base).unwrap(),
            user_agent: None,
            auth: format!("DeepL-Auth-Key {}", &key),
        }
    }

    /// Sets a user-defined HTTP client
    pub fn client(&mut self, client: reqwest::blocking::Client) -> &mut Self {
        self.client = client;
        self
    }

    /// Sets app name and version to be used in the User-Agent header, e.g. "my-app/1.2.3"
    pub fn set_app_info(&mut self, app: String) -> &mut Self {
        self.user_agent = Some(app);
        self
    }

    /// Calls the underlying client POST method
    fn post<U>(&self, url: U) -> reqwest::blocking::RequestBuilder
    where
        U: reqwest::IntoUrl,
    {
        self.client.post(url).headers(self.default_headers())
    }

    /// Calls the underlying client GET method
    fn get<U>(&self, url: U) -> reqwest::blocking::RequestBuilder
    where
        U: reqwest::IntoUrl,
    {
        self.client.get(url).headers(self.default_headers())
    }

    /// Calls the underlying client DELETE method
    fn delete<U>(&self, url: U) -> reqwest::blocking::RequestBuilder
    where
        U: reqwest::IntoUrl,
    {
        self.client.delete(url).headers(self.default_headers())
    }

    /// Construct default headers used in the request (User-Agent, Authorization)
    fn default_headers(&self) -> header::HeaderMap {
        // user agent
        let app = if let Some(s) = &self.user_agent {
            s.clone()
        } else {
            APP_USER_AGENT.to_string()
        };
        let mut map = reqwest::header::HeaderMap::new();
        map.insert(
            header::USER_AGENT,
            header::HeaderValue::from_str(&app).unwrap(),
        );

        // auth
        map.insert(
            header::AUTHORIZATION,
            header::HeaderValue::from_str(&self.auth).unwrap(),
        );
        map
    }

    /// GET /usage
    ///
    /// Get account usage
    pub fn usage(&self) -> Result<Usage, Error> {
        let url = format!("{}/usage", self.url);
        let resp = self.get(url).send().map_err(Error::Reqwest)?;
        let usage: Usage = resp.json()?;

        Ok(usage)
    }
}

#[cfg(test)]
mod test;