macro_rules! field_key {
($field:ident, $const:ident) => {
crate::types::constants::$const
};
($field:ident, $key:literal) => {
$key
};
($field:ident) => {
stringify!($field)
};
($other:ty) => {
$other
};
}
macro_rules! into_query {
(@req $url:expr, $key:expr, $value:expr, $conv:tt) => {
into_query!(@convert $url, $key, $value, $conv);
};
(@req $url:expr, $key:expr, $value:expr) => {
$url.query($key, $value);
};
(@opt $url:expr, $key:expr, $value:expr, $conv:tt) => {
if let Some(val) = $value {
into_query!(@convert $url, $key, val, $conv);
}
};
(@opt $url:expr, $key:expr, $value:expr) => {
if let Some(val) = $value {
$url.append_pair($key, val.as_ref());
}
};
(@convert $url:expr, $key:expr, $value:expr, u64) => {
let mut buffer = itoa::Buffer::new();
let s = buffer.format($value);
$url.append_pair($key, s);
};
(@convert $url:expr, $key:expr, $value:expr, bool) => {
$url.append_pair($key, &$value.to_string());
};
(@convert $url:expr, $key:expr, $value:expr, date) => {
$url.append_pair($key, &$value.to_rfc3339_opts(chrono::SecondsFormat::Secs, true));
};
(@convert $url:expr, $key:expr, $value:expr, vec) => {
for item in $value {
$url.append_pair($key, item.as_ref());
}
};
(@convert $url:expr, $key:expr, $value:expr, { $conv:expr }) => {
let converted = $conv($value);
$url.query($key, converted);
};
(@convert $url:expr, $key:expr, $value:expr, $func:ident) => {
let converted = $func($value);
$url.query($key, converted);
};
}
macro_rules! define_request {
(
$(#[$struct_meta:meta])*
$name:ident$(<$life:lifetime>)? {
$(req: {
$(
$(#[$req_meta:meta])*
$req_field:ident: $req_type:ty
$(| $req_flags:tt)?
$(=> $req_key:tt)?
$(as $req_method_name:ident)?
$(= $req_default:expr)?
$(; $req_conv:tt)?
),* $(,)?
})? $(,)?
$(opts: {
$(
$(#[$opt_meta:meta])*
$opt_field:ident: $opt_type:ty
$(| $opt_flags:tt)?
$(=> $opt_key:tt)?
$(as $opt_method_name:ident)?
$(= $opt_default:expr)?
$(; $opt_conv:tt)?
),* $(,)?
})?
$(; $($flags:ident),* $(,)?)?
}
) => {
$(#[$struct_meta])*
pub struct $name$(<$life>)? {
$($(
$(#[$req_meta])*
pub $req_field: $req_type,
)*)?
$($(
$(#[$opt_meta])*
pub $opt_field: Option<$opt_type>,
)*)?
}
impl$(<$life>)? $name$(<$life>)? {
#[allow(clippy::new_without_default)]
pub fn new(
$($(
$req_field: define_request!(@param_type $req_type $(| $req_flags)?)
),*)?
) -> Self {
Self {
$($(
$req_field: define_request!(@param_value $req_field $(| $req_flags)?),
)*)?
$($(
$opt_field: None,
)*)?
}
}
$($(
define_request!(@opt_method $opt_field: $opt_type $(| $opt_flags)?);
)*)?
}
automatic_impl!(
$name$(<$life>)? ;
req: [$($(
$req_field : $req_type $(=> $req_key)? $(; $req_conv)?
),*)?] ;
opts: [$($(
$opt_field : $opt_type $(=> $opt_key)? $(; $opt_conv)?
),*)?] ;
$($( $flags ),*)?
);
};
(@param_type $type:ty | into) => {
impl Into<$type>
};
(@param_type $type:ty) => {
$type
};
(@param_value $field:ident | into) => {
$field.into()
};
(@param_value $field:ident) => {
$field
};
(@opt_method $opt_field:ident: $opt_type:ty | into) => {
pub fn $opt_field(mut self, value: impl Into<$opt_type>) -> Self {
self.$opt_field = Some(value.into());
self
}
};
(@opt_method $opt_field:ident: $opt_type:ty) => {
pub fn $opt_field(mut self, value: $opt_type) -> Self {
self.$opt_field = Some(value);
self
}
};
}
#[cfg(any(
feature = "clips",
feature = "streams",
feature = "teams",
feature = "videos",
))]
macro_rules! define_select {
(
$(#[$struct_meta:meta])*
$name:ident$(<$life:lifetime>)? {
$(
$(#[$field_meta:meta])*
$field:ident: $field_type:ty
$(| $into:tt $(: $item_ty:ty)? )?
$(=> $key:tt)?
$(as $method_name:ident)?
$(= $default:expr)?
$(; $conv:tt)?
),* $(,)?
}
$(; $($flags:ident),*)?
) => {
$(#[$struct_meta])*
pub struct $name$(<$life>)? {
$(
$(#[$field_meta])*
pub $field: Option<$field_type>,
)*
}
impl$(<$life>)? Default for $name$(<$life>)? {
fn default() -> Self {
Self {
$(
$field: define_select!(@default_value $($default)?),
)*
}
}
}
impl$(<$life>)? $name$(<$life>)? {
#[inline]
pub fn new() -> Self {
Self::default()
}
$(
define_select!(@method $field, $field_type, $($method_name)? $(| $into $(: $item_ty)?)?);
)*
}
automatic_impl!(
$name$(<$life>)? ;
req: [] ;
opts: [$($field : $field_type $(=> $key)? $(; $conv)?),*] ;
$($( $flags ),*)?
);
};
(@default_value $default:expr) => { Some($default) };
(@default_value) => { None };
(@method $field:ident, $field_type:ty, $method_name:ident | into) => {
pub fn $method_name(value: impl Into<$field_type>) -> Self {
Self {
$field: Some(value.into()),
..Default::default()
}
}
};
(@method $field:ident, $field_type:ty, $method_name:ident) => {
pub fn $method_name(value: $field_type) -> Self {
Self {
$field: Some(value),
..Default::default()
}
}
};
(@method $field:ident, $field_type:ty, | into) => {
pub fn $field(value: impl Into<$field_type>) -> Self {
Self {
$field: Some(value.into()),
..Default::default()
}
}
};
(@method $field:ident, $field_type:ty,) => {
pub fn $field(value: $field_type) -> Self {
Self {
$field: Some(value),
..Default::default()
}
}
};
}
macro_rules! automatic_impl {
(
$name:ident$(<$life:lifetime>)? ;
req: [$($req_field:ident : $req_field_type:ty $(=> $req_key:tt)? $(; $req_conv:tt)?),*] ;
opts: [$($opt_field:ident : $opt_field_type:ty $(=> $opt_key:tt)? $(; $opt_conv:tt)?),*] ;
$($flags:ident),*
) => {
automatic_impl!(@impl
$name$(<$life>)? ;
req: [$($req_field: $req_field_type $(=> $req_key)? $(; $req_conv)?),*] ;
opts: [$($opt_field: $opt_field_type $(=> $opt_key)? $(; $opt_conv)?),*] ;
$($flags),*
);
};
(@impl $name:ident$(<$life:lifetime>)? ;
req: [$($req_field:ident : $req_field_type:ty $(=> $req_key:tt)? $(; $req_conv:tt)?),*] ;
opts: [$($opt_field:ident : $opt_field_type:ty $(=> $opt_key:tt)? $(; $opt_conv:tt)?),*] ;
into_query $(, $rest:ident)*
) => {
impl$(<$life>)? $name$(<$life>)? {
pub fn into_query(self, url: &mut url::form_urlencoded::Serializer<'_, url::UrlQuery<'_>>) {
$(
into_query!(@req url, field_key!($req_field $(, $req_key)?), self.$req_field $(, $req_conv)?);
)*
$(
into_query!(@opt url, field_key!($opt_field $(, $opt_key)?), self.$opt_field $(, $opt_conv)?);
)*
}
}
automatic_impl!(@impl $name$(<$life>)? ;
req: [$($req_field: $req_field_type $(=> $req_key)? $(; $req_conv)?),*] ;
opts: [$($opt_field: $opt_field_type $(=> $opt_key)? $(; $opt_conv)?),*] ;
$($rest),*
);
};
(@impl $name:ident$(<$life:lifetime>)? ;
req: [$($req_field:ident : $req_field_type:ty $(=> $req_key:tt)? $(; $req_conv:tt)?),*] ;
opts: [$($opt_field:ident : $opt_field_type:ty $(=> $opt_key:tt)? $(; $opt_conv:tt)?),*] ;
into_json $(, $rest:ident)*
) => {
impl$(<$life>)? $name$(<$life>)? {
pub fn into_json(self) -> Option<String> {
Some(serde_json::to_string(&self).unwrap())
}
}
automatic_impl!(@impl $name$(<$life>)? ;
req: [$($req_field: $req_field_type $(=> $req_key)? $(; $req_conv)?),*] ;
opts: [$($opt_field: $opt_field_type $(=> $opt_key)? $(; $opt_conv)?),*] ;
$($rest),*
);
};
(@impl $name:ident$(<$life:lifetime>)? ;
req: [$($req_field:ident : $req_field_type:ty $(=> $req_key:tt)? $(; $req_conv:tt)?),*] ;
opts: [$($opt_field:ident : $opt_field_type:ty $(=> $opt_key:tt)? $(; $opt_conv:tt)?),*] ;
) => {}
}
macro_rules! endpoints {
(
$(#[$trait_attr:meta])*
$trait_name:ident {
$(
$(#[$method_attr:meta])*
fn $method_name:ident$(<$($life:lifetime),* $($generics:ident $(: $bounds:tt)?),*>)?(
&self
$(, $param_name:ident: $param_type:ty )* $(,)?
) -> $return_type:ty
{
endpoint_type: $endpoint_type:ident,
method: $http_method:ident,
path: [$($path_segment:expr),* $(,)?]
$(, query_params: {$($query_config:tt)*})?
$(, headers: [$($header_config:tt)*])?
$(, body: $body_expr:expr)?
$(,)?
}
)+
}
) => {
#[allow(non_camel_case_types)]
$(#[$trait_attr])*
pub trait $trait_name {
$(
$(#[$method_attr])*
fn $method_name$(<$($life),* $($generics $(: $bounds)?),*>)?(
&self,
$($param_name: $param_type),*
) -> crate::request::TwitchAPIRequest<$return_type>;
)+
}
impl $trait_name for crate::TwitchAPI {
$(
fn $method_name$(<$($life),* $($generics $(: $bounds)?),*>)?(
&self
$(, $param_name: $param_type)*
) -> crate::request::TwitchAPIRequest<$return_type>
{
let mut url = self.build_url();
url.path_segments_mut().unwrap().extend([$($path_segment),*]);
$( endpoints!(@query url.query_pairs_mut(), $($query_config)*); )?
let headers = endpoints!(@headers self, $($($header_config)*)?);
let body = endpoints!(@body_handler $($body_expr)?);
crate::request::TwitchAPIRequest::new(
crate::request::EndpointType::$endpoint_type,
url,
reqwest::Method::$http_method,
headers,
body,
)
}
)+
}
#[cfg(test)]
mod __test_enforcement {
const _ENFORCE_ALL_TESTS_EXIST: () = {
$(
let _: fn() = super::tests::$method_name;
)*
};
}
};
(@query $url:expr, query($key:expr, $value:expr) $(, $($rest:tt)*)?) => {
$url.append_pair($key, $value.as_ref());
$(
endpoints!(@query $url, $($rest)*);
)?
};
(@query $url:expr, opt($key:expr, $value:expr) $(, $($rest:tt)*)?) => {
if let Some(val) = $value {
$url.append_pair($key, val.as_ref());
}
$(
endpoints!(@query $url, $($rest)*);
)?
};
(@query $url:expr, extend($iter:expr) $(, $($rest:tt)*)?) => {
$url.extend_pairs($iter);
$(
endpoints!(@query $url, $($rest)*);
)?
};
(@query $url:expr, opt_extend($iter:expr) $(, $($rest:tt)*)?) => {
if let Some(opts) = $iter {
$url.extend_pairs(opts);
}
$(
endpoints!(@query $url, $($rest)*);
)?
};
(@query $url:expr, pagination($pagination:expr) $(, $($rest:tt)*)?) => {
if let Some(pagination) = $pagination {
pagination.into_query(&mut $url);
}
$(
endpoints!(@query $url, $($rest)*);
)?
};
(@query $url:expr, into_query($custom_applier:expr) $(, $($rest:tt)*)?) => {
$custom_applier.into_query(&mut $url);
$(
endpoints!(@query $url, $($rest)*);
)?
};
(@query $url:expr, opt_into_query($custom_applier:expr) $(, $($rest:tt)*)?) => {
if let Some(opts) = $custom_applier {
opts.into_query(&mut $url);
}
$(
endpoints!(@query $url, $($rest)*);
)?
};
(@query $url:expr,) => {};
(@headers $self:ident, json) => {
$self.header_json()
};
(@headers $self:ident, jwt, $token:expr) => {
$self.build_jwt_headers(&$token)
};
(@headers $self:ident, ) => {
$self.default_headers()
};
(@body_handler $body_expr:expr) => {
$body_expr
};
(@body_handler) => {
None
};
}
#[cfg(test)]
macro_rules! api_test {
($endpoint:ident, $([$($param:expr),* $(,)?])?) => {
#[tokio::test]
pub(crate) async fn $endpoint() {
let suite = crate::test_utils::TwitchApiTest::new().await;
suite.$endpoint().await;
let _ = suite
.execute(|api| {
api.$endpoint($($($param),*)?)
})
.json()
.await
.unwrap();
}
};
(send $endpoint:ident, $([$($param:expr),* $(,)?])?) => {
#[tokio::test]
pub(crate) async fn $endpoint() {
let suite = crate::test_utils::TwitchApiTest::new().await;
suite.$endpoint().await;
let _ = suite
.execute(|api| {
api.$endpoint($($($param),*)?)
})
.send()
.await
.unwrap();
}
};
(extra $endpoint:ident, $test_endpoint:ident, $([$($param:expr),* $(,)?])?) => {
#[tokio::test]
async fn $test_endpoint() {
let suite = crate::test_utils::TwitchApiTest::new().await;
suite.$test_endpoint().await;
let _ = suite
.execute(|api| {
api.$endpoint($($($param),*)?)
})
.json()
.await
.unwrap();
}
};
}