#![forbid(unsafe_code)]
#![warn(missing_debug_implementations, missing_docs, clippy::cargo)]
#![allow(clippy::multiple_crate_versions, clippy::derive_partial_eq_without_eq)]
use core::fmt;
pub use http::Error as HttpError;
pub use reqwest::Error as ReqwestError;
pub use serde_json::Error as SerdeJsonError;
pub use serde_urlencoded::ser::Error as SerdeUrlencodedError;
use std::io;
use std::ops::Deref;
#[cfg(feature = "listen")]
pub use tungstenite::Error as TungsteniteError;
use reqwest::{
header::{HeaderMap, HeaderValue},
RequestBuilder,
};
use serde::de::DeserializeOwned;
use thiserror::Error;
use url::Url;
pub mod auth;
#[cfg(feature = "listen")]
pub mod common;
#[cfg(feature = "listen")]
pub mod listen;
#[cfg(feature = "manage")]
pub mod manage;
#[cfg(feature = "speak")]
pub mod speak;
static DEEPGRAM_BASE_URL: &str = "https://api.deepgram.com";
pub(crate) static USER_AGENT: &str = concat!(
env!("CARGO_PKG_NAME"),
"/",
env!("CARGO_PKG_VERSION"),
" rust",
);
#[derive(Debug, Clone)]
pub struct Transcription<'a>(#[allow(unused)] pub &'a Deepgram);
#[derive(Debug, Clone)]
pub struct Speak<'a>(#[allow(unused)] pub &'a Deepgram);
impl Deepgram {
pub fn transcription(&self) -> Transcription<'_> {
self.into()
}
pub fn text_to_speech(&self) -> Speak<'_> {
self.into()
}
}
impl<'a> From<&'a Deepgram> for Transcription<'a> {
fn from(deepgram: &'a Deepgram) -> Self {
Self(deepgram)
}
}
impl<'a> From<&'a Deepgram> for Speak<'a> {
fn from(deepgram: &'a Deepgram) -> Self {
Self(deepgram)
}
}
impl Transcription<'_> {
pub fn deepgram(&self) -> &Deepgram {
self.0
}
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) struct RedactedString(pub String);
impl fmt::Debug for RedactedString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("***")
}
}
impl Deref for RedactedString {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum AuthMethod {
ApiKey(RedactedString),
TempToken(RedactedString),
}
impl AuthMethod {
pub(crate) fn header_value(&self) -> String {
match self {
AuthMethod::ApiKey(key) => format!("Token {}", key.0),
AuthMethod::TempToken(token) => format!("Bearer {}", token.0),
}
}
}
#[derive(Debug, Clone)]
pub struct Deepgram {
#[cfg_attr(not(feature = "listen"), allow(unused))]
auth: Option<AuthMethod>,
#[cfg_attr(not(feature = "listen"), allow(unused))]
base_url: Url,
#[cfg_attr(not(feature = "listen"), allow(unused))]
client: reqwest::Client,
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum DeepgramError {
#[error("The Deepgram API returned an error.")]
DeepgramApiError {
body: String,
err: ReqwestError,
},
#[error("Something went wrong when generating the http request: {0}")]
HttpError(#[from] HttpError),
#[error("Something went wrong when making the HTTP request: {0}")]
ReqwestError(#[from] ReqwestError),
#[error("Something went wrong during I/O: {0}")]
IoError(#[from] io::Error),
#[cfg(feature = "listen")]
#[error("Something went wrong with WS: {0}")]
WsError(#[from] Box<TungsteniteError>),
#[error("Something went wrong during json serialization/deserialization: {0}")]
JsonError(#[from] SerdeJsonError),
#[error("Something went wrong during query serialization: {0}")]
UrlencodedError(#[from] SerdeUrlencodedError),
#[error("The data stream produced an error: {0}")]
StreamError(#[from] Box<dyn std::error::Error + Send + Sync + 'static>),
#[error("The provided base url is not valid")]
InvalidUrl,
#[error("websocket close frame received with error content: code: {code}, reason: {reason}")]
WebsocketClose {
code: u16,
reason: String,
},
#[error("an unepected error occurred in the deepgram client: {0}")]
InternalClientError(anyhow::Error),
#[error("The Deepgram API server response was not in the expected format: {0}")]
UnexpectedServerResponse(anyhow::Error),
}
#[cfg(feature = "listen")]
impl From<TungsteniteError> for DeepgramError {
fn from(err: TungsteniteError) -> Self {
Self::from(Box::new(err))
}
}
#[cfg_attr(not(feature = "listen"), allow(unused))]
type Result<T, E = DeepgramError> = std::result::Result<T, E>;
impl Deepgram {
pub fn new<K: AsRef<str>>(api_key: K) -> Result<Self> {
let auth = AuthMethod::ApiKey(RedactedString(api_key.as_ref().to_owned()));
let base_url = DEEPGRAM_BASE_URL.try_into().unwrap();
Self::inner_constructor(base_url, Some(auth))
}
pub fn with_temp_token<T: AsRef<str>>(temp_token: T) -> Result<Self> {
let auth = AuthMethod::TempToken(RedactedString(temp_token.as_ref().to_owned()));
let base_url = DEEPGRAM_BASE_URL.try_into().unwrap();
Self::inner_constructor(base_url, Some(auth))
}
pub fn with_base_url<U>(base_url: U) -> Result<Self>
where
U: TryInto<Url>,
U::Error: std::fmt::Debug,
{
let base_url = base_url.try_into().map_err(|_| DeepgramError::InvalidUrl)?;
Self::inner_constructor(base_url, None)
}
pub fn with_base_url_and_api_key<U, K>(base_url: U, api_key: K) -> Result<Self>
where
U: TryInto<Url>,
U::Error: std::fmt::Debug,
K: AsRef<str>,
{
let base_url = base_url.try_into().map_err(|_| DeepgramError::InvalidUrl)?;
let auth = AuthMethod::ApiKey(RedactedString(api_key.as_ref().to_owned()));
Self::inner_constructor(base_url, Some(auth))
}
pub fn with_base_url_and_temp_token<U, T>(base_url: U, temp_token: T) -> Result<Self>
where
U: TryInto<Url>,
U::Error: std::fmt::Debug,
T: AsRef<str>,
{
let base_url = base_url.try_into().map_err(|_| DeepgramError::InvalidUrl)?;
let auth = AuthMethod::TempToken(RedactedString(temp_token.as_ref().to_owned()));
Self::inner_constructor(base_url, Some(auth))
}
fn inner_constructor(base_url: Url, auth: Option<AuthMethod>) -> Result<Self> {
if base_url.cannot_be_a_base() {
return Err(DeepgramError::InvalidUrl);
}
let authorization_header = {
let mut header = HeaderMap::new();
if let Some(auth) = &auth {
let header_value = auth.header_value();
if let Ok(value) = HeaderValue::from_str(&header_value) {
header.insert("Authorization", value);
}
}
header
};
Ok(Deepgram {
auth,
base_url,
client: reqwest::Client::builder()
.user_agent(USER_AGENT)
.default_headers(authorization_header)
.build()?,
})
}
}
#[cfg_attr(not(feature = "listen"), allow(unused))]
async fn send_and_translate_response<R: DeserializeOwned>(
request_builder: RequestBuilder,
) -> crate::Result<R> {
let response = request_builder.send().await?;
match response.error_for_status_ref() {
Ok(_) => Ok(response.json().await?),
Err(err) => Err(DeepgramError::DeepgramApiError {
body: response.text().await?,
err,
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_auth_method_header_value() {
let api_key = AuthMethod::ApiKey(RedactedString("test_api_key".to_string()));
assert_eq!(api_key.header_value(), "Token test_api_key".to_string());
let temp_token = AuthMethod::TempToken(RedactedString("test_temp_token".to_string()));
assert_eq!(
temp_token.header_value(),
"Bearer test_temp_token".to_string()
);
}
#[test]
fn test_deepgram_new_with_temp_token() {
let client = Deepgram::with_temp_token("test_temp_token").unwrap();
assert_eq!(
client.auth,
Some(AuthMethod::TempToken(RedactedString(
"test_temp_token".to_string()
)))
);
}
#[test]
fn test_deepgram_new_with_api_key() {
let client = Deepgram::new("test_api_key").unwrap();
assert_eq!(
client.auth,
Some(AuthMethod::ApiKey(RedactedString(
"test_api_key".to_string()
)))
);
}
}