static ERROR_TRYFROM: &str = "no match";
macro_rules! impl_de_ser {
(@field $e:expr) => {".{}"};
($type:ident, $fmt:literal, $($field:ident),* $(,)? $(?$opt_field:ident),* $(,)?) => {
impl From<$type> for String {
fn from(t: $type) -> Self { format!(concat!($fmt, $(impl_de_ser!(@field $field),)+ $(impl_de_ser!(@field $opt_field),)*), $(t.$field,)*$(t.$opt_field.map(|f| f.to_string()).unwrap_or_default(),)*).trim_end_matches(".").to_owned() }
}
impl<'a> From<&'a $type> for String {
fn from(t: &'a $type) -> Self { format!(concat!($fmt, $(impl_de_ser!(@field $field),)+ $(impl_de_ser!(@field $opt_field),)*), $(t.$field,)*$(t.$opt_field.map(|f| f.to_string()).unwrap_or_default(),)*).trim_end_matches(".").to_owned() }
}
impl ::std::convert::TryFrom<String> for $type {
type Error = &'static str;
fn try_from(s: String) -> ::std::result::Result<Self, Self::Error> {
if s.starts_with($fmt) {
let sub_s = s.strip_prefix($fmt).ok_or("could not strip str, this should never be hit")?;
match sub_s.split('.').collect::<Vec<_>>().as_slice() {
["", $($field,)* $($opt_field,)*] => {
Ok($type {
$(
$field: $field.parse()
.map_err(|_| concat!("could not parse field <", stringify!($field), ">"))?,
)*
$(
$opt_field: Some($opt_field.parse()
.map_err(|_| concat!("could not parse field <", stringify!($opt_field), ">"))?),
)*
} )
}
#[allow(unreachable_patterns)]
["", $($field,)*] => {
Ok($type {
$(
$field: $field.parse()
.map_err(|_| concat!("could not parse field <", stringify!($field), ">"))?,
)*
$(
$opt_field: None,
)*
} )
}
_ => Err(crate::pubsub::ERROR_TRYFROM)
}
} else {
Err(crate::pubsub::ERROR_TRYFROM)
}
}
}
};
}
use serde::{Deserialize, Deserializer, Serialize};
pub mod channel_bits;
pub mod channel_bits_badge;
#[cfg(feature = "unsupported")]
#[cfg_attr(nightly, doc(cfg(feature = "unsupported")))]
pub mod channel_cheer;
pub mod channel_points;
#[cfg(feature = "unsupported")]
#[cfg_attr(nightly, doc(cfg(feature = "unsupported")))]
pub mod channel_sub_gifts;
pub mod channel_subscriptions;
#[cfg(feature = "unsupported")]
#[cfg_attr(nightly, doc(cfg(feature = "unsupported")))]
pub mod community_points;
#[cfg(feature = "unsupported")]
#[cfg_attr(nightly, doc(cfg(feature = "unsupported")))]
pub mod following;
#[cfg(feature = "unsupported")]
#[cfg_attr(nightly, doc(cfg(feature = "unsupported")))]
pub mod hypetrain;
pub mod moderation;
#[cfg(feature = "unsupported")]
#[cfg_attr(nightly, doc(cfg(feature = "unsupported")))]
pub mod raid;
#[cfg(feature = "unsupported")]
#[cfg_attr(nightly, doc(cfg(feature = "unsupported")))]
pub mod video_playback;
#[cfg_attr(nightly, doc(spotlight))]
pub trait Topic: Serialize + Into<String> {
#[cfg(feature = "twitch_oauth2")]
#[cfg_attr(nightly, doc(feature = "twitch_oauth2"))]
const SCOPE: &'static [twitch_oauth2::Scope];
}
pub enum TopicSubscribe {
Listen {
nonce: Option<String>,
topics: Vec<String>,
auth_token: String,
},
Unlisten {
nonce: Option<String>,
topics: Vec<String>,
auth_token: String,
},
}
impl Serialize for TopicSubscribe {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where S: serde::Serializer {
#[derive(Serialize)]
struct ITopicSubscribeData<'a> {
topics: &'a [String],
auth_token: &'a str,
}
#[derive(Serialize)]
struct ITopicSubscribe<'a> {
#[serde(rename = "type")]
_type: &'static str,
nonce: Option<&'a str>,
data: ITopicSubscribeData<'a>,
}
match self {
TopicSubscribe::Listen {
nonce,
topics,
auth_token,
} => ITopicSubscribe {
_type: "LISTEN",
nonce: nonce.as_deref(),
data: ITopicSubscribeData {
topics: topics.as_slice(),
auth_token,
},
}
.serialize(serializer),
TopicSubscribe::Unlisten {
nonce,
topics,
auth_token,
} => ITopicSubscribe {
_type: "UNLISTEN",
nonce: nonce.as_deref(),
data: ITopicSubscribeData {
topics: topics.as_slice(),
auth_token,
},
}
.serialize(serializer),
}
}
}
impl TopicSubscribe {
pub fn to_command(&self) -> Result<String, serde_json::Error> { serde_json::to_string(&self) }
pub fn listen<'a, T: Topic, O: Into<Option<String>>>(
topics: &'a [T],
auth_token: String,
nonce: O,
) -> TopicSubscribe
where
&'a T: Into<String>,
{
TopicSubscribe::Listen {
nonce: nonce.into(),
topics: topics.iter().map(|t| t.into()).collect(),
auth_token,
}
}
pub fn unlisten<'a, T: Topic, O: Into<Option<String>>>(
topics: &'a [T],
auth_token: String,
nonce: O,
) -> TopicSubscribe
where
&'a T: Into<String>,
{
TopicSubscribe::Unlisten {
nonce: nonce.into(),
topics: topics.iter().map(|t| t.into()).collect(),
auth_token,
}
}
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct TwitchResponse {
pub nonce: Option<String>,
pub error: Option<String>,
}
impl TwitchResponse {
pub fn is_successful(&self) -> bool { self.error.as_ref().map_or(true, |s| s.is_empty()) }
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(remote = "Self")]
pub enum TopicData {
ChannelBitsEventsV2 {
topic: channel_bits::ChannelBitsEventsV2,
#[serde(rename = "message")]
reply: Box<channel_bits::ChannelBitsEventsV2Reply>,
},
ChannelBitsBadgeUnlocks {
topic: channel_bits_badge::ChannelBitsBadgeUnlocks,
#[serde(rename = "message")]
reply: Box<channel_bits_badge::ChannelBitsBadgeUnlocksReply>,
},
ChatModeratorActions {
topic: moderation::ChatModeratorActions,
#[serde(rename = "message")]
reply: Box<moderation::ChatModeratorActionsReply>,
},
ChannelPointsChannelV1 {
topic: channel_points::ChannelPointsChannelV1,
#[serde(rename = "message")]
reply: Box<channel_points::ChannelPointsChannelV1Reply>,
},
ChannelSubscribeEventsV1 {
topic: channel_subscriptions::ChannelSubscribeEventsV1,
#[serde(rename = "message")]
reply: Box<channel_subscriptions::ChannelSubscribeEventsV1Reply>,
},
#[cfg(feature = "unsupported")]
#[cfg_attr(nightly, doc(cfg(feature = "unsupported")))]
CommunityPointsChannelV1 {
topic: community_points::CommunityPointsChannelV1,
#[serde(rename = "message")]
reply: Box<channel_points::ChannelPointsChannelV1Reply>,
},
#[cfg(feature = "unsupported")]
#[cfg_attr(nightly, doc(cfg(feature = "unsupported")))]
ChannelCheerEventsPublicV1 {
topic: channel_cheer::ChannelCheerEventsPublicV1,
#[serde(rename = "message")]
reply: Box<channel_cheer::ChannelCheerEventsPublicV1Reply>,
},
#[cfg(feature = "unsupported")]
#[cfg_attr(nightly, doc(cfg(feature = "unsupported")))]
ChannelSubGiftsV1 {
topic: channel_sub_gifts::ChannelSubGiftsV1,
#[serde(rename = "message")]
reply: Box<channel_sub_gifts::ChannelSubGiftsV1Reply>,
},
#[cfg(feature = "unsupported")]
#[cfg_attr(nightly, doc(cfg(feature = "unsupported")))]
VideoPlayback {
topic: video_playback::VideoPlayback,
#[serde(rename = "message")]
reply: Box<video_playback::VideoPlaybackReply>,
},
#[cfg(feature = "unsupported")]
#[cfg_attr(nightly, doc(cfg(feature = "unsupported")))]
VideoPlaybackById {
topic: video_playback::VideoPlaybackById,
#[serde(rename = "message")]
reply: Box<video_playback::VideoPlaybackReply>,
},
#[cfg(feature = "unsupported")]
#[cfg_attr(nightly, doc(cfg(feature = "unsupported")))]
HypeTrainEventsV1 {
topic: hypetrain::HypeTrainEventsV1,
#[serde(rename = "message")]
reply: Box<hypetrain::HypeTrainEventsV1Reply>,
},
#[cfg(feature = "unsupported")]
#[cfg_attr(nightly, doc(cfg(feature = "unsupported")))]
HypeTrainEventsV1Rewards {
topic: hypetrain::HypeTrainEventsV1Rewards,
#[serde(rename = "message")]
reply: Box<hypetrain::HypeTrainEventsV1Reply>,
},
#[cfg(feature = "unsupported")]
#[cfg_attr(nightly, doc(cfg(feature = "unsupported")))]
Following {
topic: following::Following,
#[serde(rename = "message")]
reply: Box<following::FollowingReply>,
},
#[cfg(feature = "unsupported")]
#[cfg_attr(nightly, doc(cfg(feature = "unsupported")))]
Raid {
topic: raid::Raid,
#[serde(rename = "message")]
reply: Box<raid::RaidReply>,
},
}
impl<'de> Deserialize<'de> for TopicData {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(Deserialize, PartialEq, Eq, Debug)]
#[serde(untagged)]
enum ITopicMessage {
#[cfg(feature = "unsupported")]
CommunityPointsChannelV1(community_points::CommunityPointsChannelV1),
ChannelBitsEventsV2(channel_bits::ChannelBitsEventsV2),
ChannelBitsBadgeUnlocks(channel_bits_badge::ChannelBitsBadgeUnlocks),
#[cfg(feature = "unsupported")]
ChannelCheerEventsPublicV1(channel_cheer::ChannelCheerEventsPublicV1),
#[cfg(feature = "unsupported")]
ChannelSubGiftsV1(channel_sub_gifts::ChannelSubGiftsV1),
ChatModeratorActions(moderation::ChatModeratorActions),
ChannelPointsChannelV1(channel_points::ChannelPointsChannelV1),
ChannelSubscribeEventsV1(channel_subscriptions::ChannelSubscribeEventsV1),
#[cfg(feature = "unsupported")]
VideoPlayback(video_playback::VideoPlayback),
#[cfg(feature = "unsupported")]
VideoPlaybackById(video_playback::VideoPlaybackById),
#[cfg(feature = "unsupported")]
HypeTrainEventsV1(hypetrain::HypeTrainEventsV1),
#[cfg(feature = "unsupported")]
HypeTrainEventsV1Rewards(hypetrain::HypeTrainEventsV1Rewards),
#[cfg(feature = "unsupported")]
Following(following::Following),
#[cfg(feature = "unsupported")]
Raid(raid::Raid),
}
#[derive(Deserialize, Debug)]
struct ITopicData {
topic: ITopicMessage,
message: String,
}
let reply = ITopicData::deserialize(deserializer).map_err(|e| {
serde::de::Error::custom(format!("could not deserialize topic reply: {}", e))
})?;
Ok(match reply.topic {
#[cfg(feature = "unsupported")]
ITopicMessage::CommunityPointsChannelV1(topic) => TopicData::CommunityPointsChannelV1 {
topic,
reply: serde_json::from_str(&reply.message).map_err(serde::de::Error::custom)?,
},
ITopicMessage::ChannelBitsEventsV2(topic) => TopicData::ChannelBitsEventsV2 {
topic,
reply: serde_json::from_str(&reply.message).map_err(serde::de::Error::custom)?,
},
ITopicMessage::ChannelBitsBadgeUnlocks(topic) => TopicData::ChannelBitsBadgeUnlocks {
topic,
reply: serde_json::from_str(&reply.message).map_err(serde::de::Error::custom)?,
},
#[cfg(feature = "unsupported")]
ITopicMessage::ChannelSubGiftsV1(topic) => TopicData::ChannelSubGiftsV1 {
topic,
reply: serde_json::from_str(&reply.message).map_err(serde::de::Error::custom)?,
},
#[cfg(feature = "unsupported")]
ITopicMessage::ChannelCheerEventsPublicV1(topic) => {
TopicData::ChannelCheerEventsPublicV1 {
topic,
reply: serde_json::from_str(&reply.message)
.map_err(serde::de::Error::custom)?,
}
}
ITopicMessage::ChatModeratorActions(topic) => TopicData::ChatModeratorActions {
topic,
reply: serde_json::from_str(&reply.message).map_err(serde::de::Error::custom)?,
},
ITopicMessage::ChannelPointsChannelV1(topic) => TopicData::ChannelPointsChannelV1 {
topic,
reply: serde_json::from_str(&reply.message).map_err(serde::de::Error::custom)?,
},
ITopicMessage::ChannelSubscribeEventsV1(topic) => TopicData::ChannelSubscribeEventsV1 {
topic,
reply: serde_json::from_str(&reply.message).map_err(serde::de::Error::custom)?,
},
#[cfg(feature = "unsupported")]
ITopicMessage::VideoPlayback(topic) => TopicData::VideoPlayback {
topic,
reply: serde_json::from_str(&reply.message).map_err(serde::de::Error::custom)?,
},
#[cfg(feature = "unsupported")]
ITopicMessage::VideoPlaybackById(topic) => TopicData::VideoPlaybackById {
topic,
reply: serde_json::from_str(&reply.message).map_err(serde::de::Error::custom)?,
},
#[cfg(feature = "unsupported")]
ITopicMessage::HypeTrainEventsV1(topic) => TopicData::HypeTrainEventsV1 {
topic,
reply: serde_json::from_str(&reply.message).map_err(serde::de::Error::custom)?,
},
#[cfg(feature = "unsupported")]
ITopicMessage::HypeTrainEventsV1Rewards(topic) => TopicData::HypeTrainEventsV1Rewards {
topic,
reply: serde_json::from_str(&reply.message).map_err(serde::de::Error::custom)?,
},
#[cfg(feature = "unsupported")]
ITopicMessage::Following(topic) => TopicData::Following {
topic,
reply: serde_json::from_str(&reply.message).map_err(serde::de::Error::custom)?,
},
#[cfg(feature = "unsupported")]
ITopicMessage::Raid(topic) => TopicData::Raid {
topic,
reply: serde_json::from_str(&reply.message).map_err(serde::de::Error::custom)?,
},
})
}
}
#[derive(Clone, Debug, PartialEq, Deserialize)]
#[serde(tag = "type")]
pub enum Response {
#[serde(rename = "RESPONSE")]
Response(TwitchResponse),
#[serde(rename = "MESSAGE")]
Message {
data: TopicData,
},
#[serde(rename = "PONG")]
Pong,
#[serde(rename = "RECONNECT")]
Reconnect,
}
impl Response {
pub fn parse(source: &str) -> Result<Response, serde_json::Error> {
serde_json::from_str(source)
}
}
fn deserialize_default_from_null<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
T: Deserialize<'de> + Default, {
Ok(Option::deserialize(deserializer)?.unwrap_or_default())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn error() {
let source = r#"
{
"type": "RESPONSE",
"nonce": "44h1k13746815ab1r2",
"error": ""
}
"#;
let expected = Response::Response(TwitchResponse {
nonce: Some(String::from("44h1k13746815ab1r2")),
error: Some(String::new()),
});
let actual = Response::parse(source).unwrap();
assert_eq!(expected, actual);
}
}