#![allow(clippy::module_name_repetitions)]
#[cfg(not(any(
feature = "reqwest_async",
feature = "reqwest_blocking",
feature = "ureq_blocking"
)))]
compile_error!(
"Please include EXACTLY ONE of the following client features: 'reqwest_async', 'reqwest_blocking' or 'ureq_blocking'"
);
#[cfg(any(
all(feature = "reqwest_async", feature = "reqwest_blocking"),
all(feature = "reqwest_async", feature = "ureq_blocking"),
all(feature = "reqwest_blocking", feature = "ureq_blocking")
))]
compile_error!(
"Please include ONLY ONE of the following client features: 'reqwest_async', 'reqwest_blocking' or 'ureq_blocking'"
);
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use serde_json::{Value, json};
use thiserror::Error;
pub mod card_actions;
pub mod deck_actions;
pub mod graphical_actions;
pub mod media_actions;
pub mod miscellaneous_actions;
pub mod model_actions;
pub mod note_actions;
pub mod statistic_actions;
pub mod entities;
pub mod mock;
pub mod prelude;
#[derive(Debug, Error)]
pub enum Error {
#[cfg(any(feature = "reqwest_async", feature = "reqwest_blocking"))]
#[error("send request with reqwest failed")]
Reqwest(#[from] reqwest::Error),
#[cfg(feature = "ureq_blocking")]
#[error("send request with ureq failed")]
Ureq(#[from] Box<ureq::Error>),
#[error("deserialization error")]
Serde(#[from] std::io::Error),
#[error("anki returned an unexpected error: {0}")]
Anki(String),
}
type Result<R> = std::result::Result<R, Error>;
#[derive(Deserialize)]
struct AnkiConnectResponse<R> {
result: Option<R>,
error: Option<String>,
}
pub struct AnkiClient<'a> {
pub endpoint: &'a str,
#[cfg(feature = "ureq_blocking")]
pub agent: ureq::Agent,
#[cfg(feature = "reqwest_async")]
pub client: reqwest::Client,
#[cfg(feature = "reqwest_blocking")]
pub client: reqwest::blocking::Client,
}
impl<'a> AnkiClient<'a> {
#[must_use]
pub fn new(endpoint: &'a str) -> Self {
Self {
endpoint,
#[cfg(feature = "ureq_blocking")]
agent: ureq::agent(),
#[cfg(feature = "reqwest_async")]
client: reqwest::Client::new(),
#[cfg(feature = "reqwest_blocking")]
client: reqwest::blocking::Client::new(),
}
}
}
impl Default for AnkiClient<'_> {
fn default() -> Self {
Self::new("http://localhost:8765")
}
}
#[maybe_async::sync_impl]
pub trait AnkiRequestable<Request: AnkiRequest> {
fn request(&self, params: Request) -> Result<Request::Response>;
}
#[maybe_async::async_impl]
#[async_trait::async_trait]
pub trait AnkiRequestable<Request: AnkiRequest + Send> {
async fn request(&self, params: Request) -> Result<Request::Response>;
}
pub trait AnkiRequest: std::fmt::Debug + Serialize {
type Response: Default + DeserializeOwned;
const ACTION: &'static str;
const VERSION: u8;
fn to_json(&self) -> Value {
if json!(self).is_null() {
json!({
"action": Self::ACTION,
"version": Self::VERSION,
})
} else {
json!({
"action": Self::ACTION,
"version": Self::VERSION,
"params": self,
})
}
}
}
#[maybe_async::sync_impl]
impl<Request: AnkiRequest> AnkiRequestable<Request> for AnkiClient<'_> {
fn request(&self, params: Request) -> Result<Request::Response> {
#[cfg(feature = "ureq_blocking")]
let response: AnkiConnectResponse<Request::Response> = self
.agent
.post(self.endpoint)
.send_json(params.to_json())
.map_err(|error| Error::Ureq(Box::new(error)))?
.into_body()
.read_json::<AnkiConnectResponse<Request::Response>>()
.map_err(|error| Error::Ureq(Box::new(error)))?;
#[cfg(feature = "reqwest_blocking")]
let response: AnkiConnectResponse<Request::Response> = {
self.client
.post(self.endpoint)
.json(¶ms.to_json())
.send()
.map_err(Error::Reqwest)?
.json::<AnkiConnectResponse<Request::Response>>()
.map_err(Error::Reqwest)
}?;
if let Some(error) = response.error {
Err(Error::Anki(error))
} else if let Some(result) = response.result {
Ok(result)
} else {
Ok(Default::default())
}
}
}
#[maybe_async::async_impl]
#[async_trait::async_trait]
impl<'a, Request: AnkiRequest + Send + 'a> AnkiRequestable<Request> for AnkiClient<'a> {
async fn request(&self, params: Request) -> Result<Request::Response> {
let json = params.to_json();
#[cfg(feature = "reqwest_async")]
let response: AnkiConnectResponse<Request::Response> = {
self.client
.post(self.endpoint)
.json(&json)
.send()
.await
.map_err(Error::Reqwest)?
.json::<AnkiConnectResponse<Request::Response>>()
.await
.map_err(Error::Reqwest)
}?;
if let Some(error) = response.error {
Err(Error::Anki(error))
} else if let Some(result) = response.result {
Ok(result)
} else {
Ok(Default::default())
}
}
}
pub(crate) mod serialize {
use serde::Serialize;
use std::collections::{BTreeMap, HashMap};
pub fn hashmap<S: serde::Serializer, K: serde::Serialize + Ord, V: serde::Serialize>(
value: &HashMap<K, V>,
serializer: S,
) -> Result<S::Ok, S::Error> {
let ordered: BTreeMap<_, _> = value.iter().collect();
ordered.serialize(serializer)
}
}