use serde::Deserialize;
use std::{convert::TryInto, str::FromStr};
#[cfg(feature = "twitch_oauth2")]
use twitch_oauth2::TwitchToken;
pub mod bits;
pub mod channels;
pub mod clips;
#[cfg(feature = "eventsub")]
#[cfg_attr(nightly, doc(cfg(feature = "eventsub")))]
pub mod eventsub;
pub mod games;
pub mod hypetrain;
pub mod moderation;
pub mod points;
pub mod search;
pub mod streams;
pub mod subscriptions;
pub mod tags;
pub mod users;
pub mod videos;
pub mod webhooks;
pub(crate) mod ser;
pub use ser::Error as SerializeError;
#[doc(no_inline)]
#[cfg(feature = "twitch_oauth2")]
pub use twitch_oauth2::Scope;
#[cfg(all(feature = "client"))]
#[cfg_attr(nightly, doc(cfg(all(feature = "client", feature = "helix"))))]
#[derive(Clone)]
pub struct HelixClient<'a, C>
where C: crate::HttpClient<'a> {
client: C,
_pd: std::marker::PhantomData<&'a ()>,
}
#[derive(PartialEq, Deserialize, Debug)]
struct InnerResponse<D> {
data: D,
#[serde(default)]
pagination: Pagination,
}
#[derive(Deserialize, Clone, Debug)]
struct HelixRequestError {
error: String,
status: u16,
message: String,
}
#[cfg(feature = "client")]
impl<'a, C: crate::HttpClient<'a>> HelixClient<'a, C> {
pub fn with_client(client: C) -> HelixClient<'a, C> {
HelixClient {
client,
_pd: std::marker::PhantomData::default(),
}
}
pub fn new() -> HelixClient<'a, C>
where C: Default {
let client = C::default();
HelixClient::with_client(client)
}
pub fn clone_client(&self) -> C
where C: Clone {
self.client.clone()
}
pub async fn req_get<R, D, T>(
&'a self,
request: R,
token: &T,
) -> Result<Response<R, D>, ClientRequestError<<C as crate::HttpClient<'a>>::Error>>
where
R: Request<Response = D> + Request + RequestGet,
D: serde::de::DeserializeOwned + PartialEq,
T: TwitchToken + ?Sized,
{
let req = request.create_request(token.token().secret(), token.client_id().as_str())?;
let uri = req.uri().clone();
let response = self
.client
.req(req)
.await
.map_err(ClientRequestError::RequestError)?;
<R>::parse_response(Some(request), &uri, response).map_err(Into::into)
}
pub async fn req_post<R, B, D, T>(
&'a self,
request: R,
body: B,
token: &T,
) -> Result<Response<R, D>, ClientRequestError<<C as crate::HttpClient<'a>>::Error>>
where
R: Request<Response = D> + Request + RequestPost<Body = B>,
B: serde::Serialize,
D: serde::de::DeserializeOwned + PartialEq,
T: TwitchToken + ?Sized,
{
let req =
request.create_request(body, token.token().secret(), token.client_id().as_str())?;
let uri = req.uri().clone();
let response = self
.client
.req(req)
.await
.map_err(ClientRequestError::RequestError)?;
<R>::parse_response(Some(request), &uri, response).map_err(Into::into)
}
pub async fn req_patch<R, B, D, T>(
&'a self,
request: R,
body: B,
token: &T,
) -> Result<D, ClientRequestError<<C as crate::HttpClient<'a>>::Error>>
where
R: Request<Response = D> + Request + RequestPatch<Body = B>,
B: serde::Serialize,
D: std::convert::TryFrom<http::StatusCode, Error = std::borrow::Cow<'static, str>>
+ serde::de::DeserializeOwned
+ PartialEq,
T: TwitchToken + ?Sized,
{
let req =
request.create_request(body, token.token().secret(), token.client_id().as_str())?;
let uri = req.uri().clone();
let response = self
.client
.req(req)
.await
.map_err(ClientRequestError::RequestError)?;
<R>::parse_response(&uri, response).map_err(Into::into)
}
pub async fn req_delete<R, D, T>(
&'a self,
request: R,
token: &T,
) -> Result<D, ClientRequestError<<C as crate::HttpClient<'a>>::Error>>
where
R: Request<Response = D> + Request + RequestDelete,
D: std::convert::TryFrom<http::StatusCode, Error = std::borrow::Cow<'static, str>>
+ serde::de::DeserializeOwned
+ PartialEq,
T: TwitchToken + ?Sized,
{
let req = request.create_request(token.token().secret(), token.client_id().as_str())?;
let uri = req.uri().clone();
let response = self
.client
.req(req)
.await
.map_err(ClientRequestError::RequestError)?;
<R>::parse_response(&uri, response).map_err(Into::into)
}
pub async fn req_put<R, D, T>(
&'a self,
request: R,
token: &T,
) -> Result<D, ClientRequestError<<C as crate::HttpClient<'a>>::Error>>
where
R: Request<Response = D> + Request + RequestPut,
D: std::convert::TryFrom<http::StatusCode, Error = std::borrow::Cow<'static, str>>
+ serde::de::DeserializeOwned
+ PartialEq,
T: TwitchToken + ?Sized,
{
let req = request.create_request(token.token().secret(), token.client_id().as_str())?;
let uri = req.uri().clone();
let response = self
.client
.req(req)
.await
.map_err(ClientRequestError::RequestError)?;
<R>::parse_response(&uri, response).map_err(Into::into)
}
}
#[cfg(feature = "client")]
impl<'a, C> Default for HelixClient<'a, C>
where C: crate::HttpClient<'a> + Default
{
fn default() -> HelixClient<'a, C> { HelixClient::new() }
}
fn deserialize_default_from_empty_string<'de, D, T>(
deserializer: D,
) -> Result<Option<T>, D::Error>
where
D: serde::de::Deserializer<'de>,
T: serde::de::DeserializeOwned + Default, {
let val = serde_json::Value::deserialize(deserializer)?;
match val {
serde_json::Value::String(string) if string.is_empty() => Ok(None),
other => Ok(serde_json::from_value(other).map_err(serde::de::Error::custom)?),
}
}
#[async_trait::async_trait]
#[cfg_attr(nightly, doc(spotlight))]
pub trait Request: serde::Serialize {
const PATH: &'static str;
#[cfg(feature = "twitch_oauth2")]
const SCOPE: &'static [twitch_oauth2::Scope];
#[cfg(feature = "twitch_oauth2")]
const OPT_SCOPE: &'static [twitch_oauth2::Scope] = &[];
type Response: serde::de::DeserializeOwned + PartialEq;
fn query(&self) -> Result<String, ser::Error> { ser::to_string(&self) }
fn get_uri(&self) -> Result<http::Uri, InvalidUri> {
http::Uri::from_str(&format!(
"{}{}?{}",
crate::TWITCH_HELIX_URL,
<Self as Request>::PATH,
self.query()?
))
.map_err(Into::into)
}
fn get_bare_uri() -> Result<http::Uri, InvalidUri> {
http::Uri::from_str(&format!(
"{}{}?",
crate::TWITCH_HELIX_URL,
<Self as Request>::PATH,
))
.map_err(Into::into)
}
}
#[cfg_attr(nightly, doc(spotlight))]
pub trait RequestPost: Request {
type Body: serde::Serialize;
fn body(&self, body: &Self::Body) -> Result<String, BodyError> {
serde_json::to_string(body).map_err(Into::into)
}
fn create_request(
&self,
body: Self::Body,
token: &str,
client_id: &str,
) -> Result<http::Request<Vec<u8>>, CreateRequestError> {
let uri = self.get_uri()?;
let body = self.body(&body)?;
let mut bearer =
http::HeaderValue::from_str(&format!("Bearer {}", token)).map_err(|_| {
CreateRequestError::Custom("Could not make token into headervalue".into())
})?;
bearer.set_sensitive(true);
http::Request::builder()
.method(http::Method::POST)
.uri(uri)
.header("Client-ID", client_id)
.header("Content-Type", "application/json")
.header(http::header::AUTHORIZATION, bearer)
.body(body.into_bytes())
.map_err(Into::into)
}
fn parse_response(
request: Option<Self>,
uri: &http::Uri,
response: http::Response<Vec<u8>>,
) -> Result<Response<Self, <Self as Request>::Response>, HelixRequestPostError>
where
Self: Sized,
{
let text = std::str::from_utf8(&response.body()).map_err(|e| {
HelixRequestPostError::Utf8Error(response.body().clone(), e, uri.clone())
})?;
if let Ok(HelixRequestError {
error,
status,
message,
}) = serde_json::from_str::<HelixRequestError>(&text)
{
return Err(HelixRequestPostError::Error {
error,
status: status.try_into().unwrap_or(http::StatusCode::BAD_REQUEST),
message,
uri: uri.clone(),
body: response.body().clone(),
});
}
let response: InnerResponse<<Self as Request>::Response> = serde_json::from_str(&text)
.map_err(|e| {
HelixRequestPostError::DeserializeError(text.to_string(), e, uri.clone())
})?;
Ok(Response {
data: response.data,
pagination: response.pagination.cursor,
request,
})
}
}
#[cfg_attr(nightly, doc(spotlight))]
pub trait RequestPatch: Request
where <Self as Request>::Response:
std::convert::TryFrom<http::StatusCode, Error = std::borrow::Cow<'static, str>> {
type Body: serde::Serialize;
fn body(&self, body: &Self::Body) -> Result<String, BodyError> {
serde_json::to_string(body).map_err(Into::into)
}
fn create_request(
&self,
body: Self::Body,
token: &str,
client_id: &str,
) -> Result<http::Request<Vec<u8>>, CreateRequestError> {
let uri = self.get_uri()?;
let body = self.body(&body)?;
let mut bearer =
http::HeaderValue::from_str(&format!("Bearer {}", token)).map_err(|_| {
CreateRequestError::Custom("Could not make token into headervalue".into())
})?;
bearer.set_sensitive(true);
http::Request::builder()
.method(http::Method::PATCH)
.uri(uri)
.header("Client-ID", client_id)
.header("Content-Type", "application/json")
.header(http::header::AUTHORIZATION, bearer)
.body(body.into_bytes())
.map_err(Into::into)
}
fn parse_response(
uri: &http::Uri,
response: http::Response<Vec<u8>>,
) -> Result<<Self as Request>::Response, HelixRequestPatchError>
where
Self: Sized,
{
match response.status().try_into() {
Ok(result) => Ok(result),
Err(err) => Err(HelixRequestPatchError {
status: response.status(),
message: err.to_string(),
uri: uri.clone(),
body: response.body().clone(),
}),
}
}
}
#[cfg_attr(nightly, doc(spotlight))]
pub trait RequestDelete: Request {
fn create_request(
&self,
token: &str,
client_id: &str,
) -> Result<http::Request<Vec<u8>>, CreateRequestError> {
let uri = self.get_uri()?;
let mut bearer =
http::HeaderValue::from_str(&format!("Bearer {}", token)).map_err(|_| {
CreateRequestError::Custom("Could not make token into headervalue".into())
})?;
bearer.set_sensitive(true);
http::Request::builder()
.method(http::Method::DELETE)
.uri(uri)
.header("Client-ID", client_id)
.header("Content-Type", "application/json")
.header(http::header::AUTHORIZATION, bearer)
.body(Vec::with_capacity(0))
.map_err(Into::into)
}
fn parse_response(
uri: &http::Uri,
response: http::Response<Vec<u8>>,
) -> Result<<Self as Request>::Response, HelixRequestDeleteError>
where
<Self as Request>::Response:
std::convert::TryFrom<http::StatusCode, Error = std::borrow::Cow<'static, str>>,
Self: Sized,
{
let text = std::str::from_utf8(&response.body()).map_err(|e| {
HelixRequestDeleteError::Utf8Error(response.body().clone(), e, uri.clone())
})?;
if let Ok(HelixRequestError {
error,
status,
message,
}) = serde_json::from_str::<HelixRequestError>(&text)
{
return Err(HelixRequestDeleteError::Error {
error,
status: status.try_into().unwrap_or(http::StatusCode::BAD_REQUEST),
message,
uri: uri.clone(),
});
}
match response.status().try_into() {
Ok(result) => Ok(result),
Err(err) => Err(HelixRequestDeleteError::Error {
error: String::new(),
status: response.status(),
message: err.to_string(),
uri: uri.clone(),
}),
}
}
}
#[cfg_attr(nightly, doc(spotlight))]
pub trait RequestPut: Request {
fn create_request(
&self,
token: &str,
client_id: &str,
) -> Result<http::Request<Vec<u8>>, CreateRequestError> {
let uri = self.get_uri()?;
let mut bearer =
http::HeaderValue::from_str(&format!("Bearer {}", token)).map_err(|_| {
CreateRequestError::Custom("Could not make token into headervalue".into())
})?;
bearer.set_sensitive(true);
http::Request::builder()
.method(http::Method::PUT)
.uri(uri)
.header("Client-ID", client_id)
.header("Content-Type", "application/json")
.header(http::header::AUTHORIZATION, bearer)
.body(Vec::with_capacity(0))
.map_err(Into::into)
}
fn parse_response(
uri: &http::Uri,
response: http::Response<Vec<u8>>,
) -> Result<<Self as Request>::Response, HelixRequestPutError>
where
<Self as Request>::Response:
std::convert::TryFrom<http::StatusCode, Error = std::borrow::Cow<'static, str>>,
Self: Sized,
{
let text = std::str::from_utf8(&response.body()).map_err(|e| {
HelixRequestPutError::Utf8Error(response.body().clone(), e, uri.clone())
})?;
if let Ok(HelixRequestError {
error,
status,
message,
}) = serde_json::from_str::<HelixRequestError>(&text)
{
return Err(HelixRequestPutError::Error {
error,
status: status.try_into().unwrap_or(http::StatusCode::BAD_REQUEST),
message,
uri: uri.clone(),
});
}
match response.status().try_into() {
Ok(result) => Ok(result),
Err(err) => Err(HelixRequestPutError::Error {
error: String::new(),
status: response.status(),
message: err.to_string(),
uri: uri.clone(),
}),
}
}
}
#[cfg_attr(nightly, doc(spotlight))]
pub trait RequestGet: Request {
fn create_request(
&self,
token: &str,
client_id: &str,
) -> Result<http::Request<Vec<u8>>, CreateRequestError> {
let uri = self.get_uri()?;
let mut bearer =
http::HeaderValue::from_str(&format!("Bearer {}", token)).map_err(|_| {
CreateRequestError::Custom("Could not make token into headervalue".into())
})?;
bearer.set_sensitive(true);
http::Request::builder()
.method(http::Method::GET)
.uri(uri)
.header("Client-ID", client_id)
.header("Content-Type", "application/json")
.header(http::header::AUTHORIZATION, bearer)
.body(Vec::with_capacity(0))
.map_err(Into::into)
}
fn parse_response(
request: Option<Self>,
uri: &http::Uri,
response: http::Response<Vec<u8>>,
) -> Result<Response<Self, <Self as Request>::Response>, HelixRequestGetError>
where
Self: Sized,
{
let text = std::str::from_utf8(&response.body()).map_err(|e| {
HelixRequestGetError::Utf8Error(response.body().clone(), e, uri.clone())
})?;
if let Ok(HelixRequestError {
error,
status,
message,
}) = serde_json::from_str::<HelixRequestError>(&text)
{
return Err(HelixRequestGetError::Error {
error,
status: status.try_into().unwrap_or(http::StatusCode::BAD_REQUEST),
message,
uri: uri.clone(),
});
}
let response: InnerResponse<_> = serde_json::from_str(&text).map_err(|e| {
HelixRequestGetError::DeserializeError(text.to_string(), e, uri.clone())
})?;
Ok(Response {
data: response.data,
pagination: response.pagination.cursor,
request,
})
}
}
#[derive(PartialEq, Debug)]
pub struct Response<R, D>
where
R: Request<Response = D>,
D: serde::de::DeserializeOwned + PartialEq, {
pub data: D,
pub pagination: Option<Cursor>,
pub request: Option<R>,
}
#[cfg(feature = "client")]
impl<R, D> Response<R, D>
where
R: Request<Response = D> + Clone + Paginated + RequestGet + std::fmt::Debug,
D: serde::de::DeserializeOwned + std::fmt::Debug + PartialEq,
{
pub async fn get_next<'a, C: crate::HttpClient<'a>>(
self,
client: &'a HelixClient<'a, C>,
token: &impl TwitchToken,
) -> Result<Option<Response<R, D>>, ClientRequestError<<C as crate::HttpClient<'a>>::Error>>
{
if let Some(mut req) = self.request.clone() {
if self.pagination.is_some() {
req.set_pagination(self.pagination);
let res = client.req_get(req, token).await.map(Some);
if let Ok(Some(r)) = res {
if r.data == self.data {
Ok(None)
} else {
Ok(Some(r))
}
} else {
res
}
} else {
Ok(None)
}
} else {
Err(ClientRequestError::Custom(
"no source request attached".into(),
))
}
}
}
pub trait Paginated: Request {
fn set_pagination(&mut self, cursor: Option<Cursor>);
}
#[derive(PartialEq, Deserialize, Debug, Clone, Default)]
struct Pagination {
#[serde(default)]
cursor: Option<Cursor>,
}
pub type Cursor = String;
#[derive(thiserror::Error, Debug, displaydoc::Display)]
pub enum ClientRequestError<RE: std::error::Error + Send + Sync + 'static> {
RequestError(RE),
NoPage,
CreateRequestError(#[from] CreateRequestError),
HelixRequestGetError(#[from] HelixRequestGetError),
HelixRequestPutError(#[from] HelixRequestPutError),
HelixRequestPostError(#[from] HelixRequestPostError),
HelixRequestPatchError(#[from] HelixRequestPatchError),
HelixRequestDeleteError(#[from] HelixRequestDeleteError),
Custom(std::borrow::Cow<'static, str>),
}
#[derive(thiserror::Error, Debug, displaydoc::Display)]
pub enum CreateRequestError {
HttpError(#[from] http::Error),
SerializeError(#[from] BodyError),
InvalidUri(#[from] InvalidUri),
Custom(std::borrow::Cow<'static, str>),
}
#[derive(thiserror::Error, Debug, displaydoc::Display)]
pub enum InvalidUri {
UriParseError(#[from] http::uri::InvalidUri),
QuerySerializeError(#[from] ser::Error),
}
#[derive(thiserror::Error, Debug, displaydoc::Display)]
pub enum HelixRequestGetError {
Error {
error: String,
status: http::StatusCode,
message: String,
uri: http::Uri,
},
Utf8Error(Vec<u8>, #[source] std::str::Utf8Error, http::Uri),
DeserializeError(String, #[source] serde_json::Error, http::Uri),
InvalidUri(#[from] InvalidUri),
}
#[derive(thiserror::Error, Debug, displaydoc::Display)]
pub enum HelixRequestPutError {
Error {
error: String,
status: http::StatusCode,
message: String,
uri: http::Uri,
},
Utf8Error(Vec<u8>, #[source] std::str::Utf8Error, http::Uri),
DeserializeError(String, #[source] serde_json::Error, http::Uri),
}
#[derive(thiserror::Error, Debug, displaydoc::Display)]
pub enum HelixRequestPostError {
Error {
error: String,
status: http::StatusCode,
message: String,
uri: http::Uri,
body: Vec<u8>,
},
Utf8Error(Vec<u8>, #[source] std::str::Utf8Error, http::Uri),
DeserializeError(String, #[source] serde_json::Error, http::Uri),
}
#[derive(thiserror::Error, Debug, displaydoc::Display)]
pub struct HelixRequestPatchError {
status: http::StatusCode,
message: String,
uri: http::Uri,
body: Vec<u8>,
}
#[derive(thiserror::Error, Debug, displaydoc::Display)]
pub enum HelixRequestDeleteError {
Error {
error: String,
status: http::StatusCode,
message: String,
uri: http::Uri,
},
Utf8Error(Vec<u8>, #[source] std::str::Utf8Error, http::Uri),
}
#[derive(thiserror::Error, Debug, displaydoc::Display)]
pub enum BodyError {
JsonError(#[from] serde_json::Error),
QuerySerializeError(#[from] ser::Error),
InvalidUri(#[from] InvalidUri),
}