use crate::{
endpoint::{Error, Result, REPO_URL},
DeepLApi, Lang,
};
use core::future::IntoFuture;
use std::borrow::Borrow;
use std::collections::HashMap;
use typed_builder::TypedBuilder;
use super::Pollable;
#[derive(Debug, TypedBuilder)]
#[builder(build_method(name = send))]
pub struct CreateGlossary<'a> {
client: &'a DeepLApi,
name: String,
source_lang: Lang,
target_lang: Lang,
#[builder(setter(prefix = "__"))]
entries: Vec<(String, String)>,
#[builder(default = EntriesFormat::TSV)]
format: EntriesFormat,
}
#[allow(non_camel_case_types)]
impl<'a, _c, _n, _s, _t, _f> CreateGlossaryBuilder<'a, (_c, _n, _s, _t, (), _f)> {
pub fn entries<S, T, B, I>(
self,
iter: I,
) -> CreateGlossaryBuilder<'a, (_c, _n, _s, _t, (Vec<(String, String)>,), _f)>
where
S: ToString,
T: ToString,
B: Borrow<(S, T)>,
I: IntoIterator<Item = B>,
{
let entries = iter
.into_iter()
.map(|t| (t.borrow().0.to_string(), t.borrow().1.to_string()))
.collect();
let (client, name, source_lang, target_lang, (), format) = self.fields;
CreateGlossaryBuilder {
fields: (client, name, source_lang, target_lang, (entries,), format),
phantom: self.phantom,
}
}
}
type CreateGlossaryBuilderStart<'a> =
CreateGlossaryBuilder<'a, ((&'a DeepLApi,), (String,), (), (), (), ())>;
impl<'a> IntoFuture for CreateGlossary<'a> {
type Output = Result<GlossaryResp>;
type IntoFuture = Pollable<'a, Self::Output>;
fn into_future(self) -> Self::IntoFuture {
let client = self.client.clone();
let fields = CreateGlossaryRequestParam::from(self);
let fut = async move {
let resp = client
.post(client.get_endpoint("glossaries"))
.json(&fields)
.send()
.await
.map_err(|err| Error::RequestFail(err.to_string()))?
.json::<GlossaryPossibleResps>()
.await
.unwrap_or_else(|_| {
panic!(
"Unmatched response to CreateGlossaryResp, please open issue on {REPO_URL}."
)
});
match resp {
GlossaryPossibleResps::Fail { message } => Err(Error::RequestFail(format!(
"Fail to create request to glossary API: {message}"
))),
GlossaryPossibleResps::Success {
glossary_id,
name,
ready,
source_lang,
target_lang,
creation_time,
entry_count,
} => Ok(GlossaryResp {
glossary_id,
name,
ready,
source_lang,
target_lang,
creation_time,
entry_count,
}),
}
};
Box::pin(fut)
}
}
#[derive(serde::Deserialize)]
#[serde(untagged)]
enum GlossaryPossibleResps {
Success {
glossary_id: String,
name: String,
ready: bool,
source_lang: Lang,
target_lang: Lang,
creation_time: String,
entry_count: u64,
},
Fail {
message: String,
},
}
#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct GlossaryResp {
pub glossary_id: String,
pub name: String,
pub ready: bool,
pub source_lang: Lang,
pub target_lang: Lang,
pub creation_time: String,
pub entry_count: u64,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct CreateGlossaryRequestParam {
name: String,
source_lang: String,
target_lang: String,
entries: String,
entries_format: String,
}
impl<'a> From<CreateGlossary<'a>> for CreateGlossaryRequestParam {
fn from(value: CreateGlossary<'a>) -> Self {
CreateGlossaryRequestParam {
name: value.name,
source_lang: value.source_lang.to_string().to_lowercase(),
target_lang: value.target_lang.to_string().to_lowercase(),
entries: match value.format {
EntriesFormat::TSV => value
.entries
.iter()
.map(|(x, y)| format!("{x}\t{y}"))
.collect::<Vec<String>>()
.join("\n"),
EntriesFormat::CSV => value
.entries
.iter()
.map(|(x, y)| format!("{x},{y}"))
.collect::<Vec<String>>()
.join("\n"),
},
entries_format: value.format.to_string(),
}
}
}
#[derive(Debug)]
pub enum EntriesFormat {
TSV,
CSV,
}
impl ToString for EntriesFormat {
fn to_string(&self) -> String {
match self {
EntriesFormat::TSV => "tsv".to_string(),
EntriesFormat::CSV => "csv".to_string(),
}
}
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct GlossaryLanguagePair {
pub source_lang: Lang,
pub target_lang: Lang,
}
impl DeepLApi {
pub fn create_glossary(&self, name: impl ToString) -> CreateGlossaryBuilderStart {
CreateGlossary::builder()
.client(self)
.name(name.to_string())
}
pub async fn list_all_glossaries(&self) -> Result<Vec<GlossaryResp>> {
Ok(
self.get(self.get_endpoint("glossaries"))
.send()
.await
.map_err(|e| Error::RequestFail(e.to_string()))?
.json::<HashMap<String, Vec<GlossaryResp>>>()
.await
.map_err(|err| Error::RequestFail(format!("Unexpected error when requesting list_all_glossaries, please open issue on {REPO_URL}: {err}")))?
.remove("glossaries")
.ok_or(Error::RequestFail(format!("Unable to find key glossaries in response, please open issue on {REPO_URL}")))?
)
}
pub async fn retrieve_glossary_details(&self, id: impl ToString) -> Result<GlossaryResp> {
match self
.get(self.get_endpoint(&format!("glossaries/{}", id.to_string())))
.send()
.await
.map_err(|e| Error::RequestFail(e.to_string()))?
.json::<GlossaryPossibleResps>()
.await
.expect("")
{
GlossaryPossibleResps::Fail { message } => Err(Error::RequestFail(format!(
"fail to send request to glossary API: {message}"
))),
GlossaryPossibleResps::Success {
glossary_id,
name,
ready,
source_lang,
target_lang,
creation_time,
entry_count,
} => Ok(GlossaryResp {
glossary_id,
name,
ready,
source_lang,
target_lang,
creation_time,
entry_count,
}),
}
}
pub async fn delete_glossary(&self, id: impl ToString) -> Result<()> {
self.del(self.get_endpoint(&format!("glossaries/{}", id.to_string())))
.send()
.await
.map_err(|e| Error::RequestFail(e.to_string()))
.map(|_| ())
}
pub async fn retrieve_glossary_entries(
&self,
id: impl ToString,
) -> Result<Vec<(String, String)>> {
Ok(self
.get(self.get_endpoint(&format!("glossaries/{}/entries", id.to_string())))
.header("Accept", "text/tab-separated-values")
.send()
.await
.map_err(|e| Error::RequestFail(e.to_string()))?
.text()
.await
.map(|resp| {
resp.split("\n")
.map(|line| {
let mut pair = line.split("\t");
(
pair.next().unwrap().to_string(),
pair.next().unwrap().to_string(),
)
})
.collect()
})
.map_err(|err| {
Error::RequestFail(format!("fail to retrieve glossary entries: {err}"))
}))?
}
pub async fn list_glossary_language_pairs(&self) -> Result<Vec<GlossaryLanguagePair>> {
let pair = self
.get(self.get_endpoint("glossary-language-pairs"))
.send()
.await
.map_err(|e| Error::RequestFail(e.to_string()))?
.json::<HashMap<String, Vec<GlossaryLanguagePair>>>()
.await
.map_err(|err| {
Error::RequestFail(format!("fail to list glossary language pairs: {err}"))
})?
.remove("supported_languages")
.ok_or(Error::RequestFail(format!(
"Fail to get supported languages from glossary language pairs"
)))?;
Ok(pair)
}
}
#[tokio::test]
async fn test_glossary_api() {
use crate::{glossary::EntriesFormat, DeepLApi, Lang};
let key = std::env::var("DEEPL_API_KEY").unwrap();
let deepl = DeepLApi::with(&key).new();
assert_ne!(deepl.list_glossary_language_pairs().await.unwrap().len(), 0);
let my_entries = vec![("Hello", "Guten Tag"), ("Bye", "Auf Wiedersehen")];
let resp = deepl
.create_glossary("My Glossary")
.source_lang(Lang::EN)
.target_lang(Lang::DE)
.entries(&my_entries)
.format(EntriesFormat::CSV) .send()
.await
.unwrap();
assert_eq!(resp.name, "My Glossary");
let all = deepl.list_all_glossaries().await.unwrap();
assert_ne!(all.len(), 0);
let detail = deepl
.retrieve_glossary_details(&resp.glossary_id)
.await
.unwrap();
assert_eq!(detail, resp);
let entries = deepl
.retrieve_glossary_entries(&resp.glossary_id)
.await
.unwrap();
assert_eq!(entries.len(), 2);
let entries: HashMap<String, String> = HashMap::from_iter(entries);
assert_eq!(entries["Hello"], "Guten Tag");
assert_eq!(entries["Bye"], "Auf Wiedersehen");
deepl.delete_glossary(resp.glossary_id).await.unwrap();
}