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(),
		)),
	}
}