1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
//! Channel search request // Imports use crate::{helix_url, HelixRequest}; use reqwest as req; /// Channel search request /// /// This request uses the `/search/channels` path /// to search channels by a query string. /// /// Response is a list of `[Channel]s`. /// /// # Examples /// Simple request: /// ``` /// # use twitch_helix::request::search::channel::Request; /// # use twitch_helix::HelixRequest; /// let mut request = Request::new("my-channel"); /// /// let url = request.url(); /// assert_eq!(url.host_str(), Some("api.twitch.tv")); /// assert_eq!(url.path(), "/helix/search/channels"); /// assert_eq!(url.query(), Some("query=my-channel")); /// ``` /// /// Using every argument: /// ``` /// # use twitch_helix::request::search::channel::Request; /// # use twitch_helix::HelixRequest; /// let mut request = Request::new("my-channel"); /// request.first = Some(100); /// request.after = Some("my-cursor".to_string()); /// request.live_only = Some(true); /// /// let url = request.url(); /// assert_eq!(url.host_str(), Some("api.twitch.tv")); /// assert_eq!(url.path(), "/helix/search/channels"); /// assert_eq!(url.query(), Some("query=my-channel&first=100&after=my-cursor&live_only=true")); /// ``` #[derive(PartialEq, Eq, Clone, Debug)] pub struct Request { /// Search query pub query: String, /// Maximum number of objects to return pub first: Option<usize>, /// Cursor for forward pagination pub after: Option<String>, /// Filter results for live streams only. pub live_only: Option<bool>, } impl Request { /// Creates a new channel search request given /// the query to search for pub fn new(query: impl Into<String>) -> Self { Self { query: query.into(), first: None, after: None, live_only: None, } } /// Finds the exact channel requested given the response /// /// Attempts to find an exact match in the `display_name` /// field of the channel, without considering case. #[must_use] pub fn channel(&self, channels: Vec<Channel>) -> Option<Channel> { // Check every channel in the response for channel in channels { if unicase::eq(&self.query, &channel.display_name) { return Some(channel); } } // If we get here, no channel was found None } /// Finds the exact channel requested given the response by reference. /// /// See [`Self::channel`] for more information. #[must_use] pub fn channel_ref<'a>(&self, channels: &'a [Channel]) -> Option<&'a Channel> { // Check every channel in the response for channel in channels { if unicase::eq(&self.query, &channel.display_name) { return Some(channel); } } // If we get here, no channel was found None } } impl HelixRequest for Request { type Response = Vec<Channel>; fn url(&self) -> url::Url { // Append all our arguments if they exist let mut url = helix_url!(search / channels); let mut query_pairs = url.query_pairs_mut(); query_pairs.append_pair("query", &self.query); if let Some(first) = &self.first { query_pairs.append_pair("first", &first.to_string()); } if let Some(after) = &self.after { query_pairs.append_pair("after", after); } if let Some(live_only) = &self.live_only { query_pairs.append_pair("live_only", &live_only.to_string()); } // Drop the query pairs and return the url std::mem::drop(query_pairs); url } fn http_method(&self) -> req::Method { req::Method::GET } } /// Each channel in the output data #[derive(PartialEq, Eq, Clone, Debug)] #[derive(serde::Serialize, serde::Deserialize)] pub struct Channel { /// Channel language pub broadcaster_language: String, /// Display name pub display_name: String, /// Game id pub game_id: String, /// Channel id pub id: String, /// Live status pub is_live: bool, /// Tag IDs that apply to the stream. /// Note: Category tags are not returned pub tag_ids: Vec<String>, /// Thumbnail url pub thumbnail_url: String, /// Title pub title: String, /// UTC timestamp for stream start /// Live streams only. // TODO: Deserialize with our custom function too. #[serde(deserialize_with = "deserialize_channel_start_at")] pub started_at: Option<chrono::DateTime<chrono::Utc>>, } /// Deserializer for [`Channel::started_at`] /// /// # Example /// ``` /// # use twitch_helix::request::search::channel::deserialize_channel_start_at; /// use chrono::{Datelike, Timelike}; /// let mut deserializer = serde_json::Deserializer::from_str("\"2020-07-23T14:49:33Z\""); /// let res = deserialize_channel_start_at(&mut deserializer) /// .expect("Unable to parse utc date-time") /// .expect("Parsed no utc time-date from a non-empty string"); /// assert_eq!(res.year(), 2020); /// assert_eq!(res.month(), 07); /// assert_eq!(res.day(), 23); /// assert_eq!(res.hour(), 14); /// assert_eq!(res.minute(), 49); /// assert_eq!(res.second(), 33); /// ``` #[doc(hidden)] // Required until we get a `pub(test)` or some macro that can do it pub fn deserialize_channel_start_at<'de, D>(deserializer: D) -> Result<Option<chrono::DateTime<chrono::Utc>>, D::Error> where D: serde::Deserializer<'de>, { // Deserialize as a string let started_at = <String as serde::Deserialize>::deserialize(deserializer)?; // If it's empty, return `None` if started_at.is_empty() { return Ok(None); } // Else try to parse it as a `Utc` match started_at.parse() { Ok(started_at) => Ok(Some(started_at)), // On error, give an `invalid_value` error. Err(err) => Err(<D::Error as serde::de::Error>::invalid_value( serde::de::Unexpected::Str(&started_at), &format!("Unable to parse time as `DateTime<Utc>`: {}", err).as_str(), )), } }