Skip to content

Commit 6f487eb

Browse files
Merge #155
155: refactor client defaults r=Emilgardis a=Emilgardis Co-authored-by: Emil Gardström <[email protected]>
2 parents 25c4ab2 + 9aba550 commit 6f487eb

15 files changed

+212
-47
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@
2424
* Deprecated specific term actions in `ChatModeratorActionsReply`, replacing them with `ChannelTermsAction`
2525
* Deprecated `Vip` action in `ChatModeratorActionsReply`, replacing it with `VipAdded`
2626
* Removed some derived impls and fixed builders that assumed a default wrongly.
27+
* `HelixClient::new`, `TmiClient::new` and `TwitchClient::new` now give a more specified client.
2728

2829
### Removed
2930

3031
* Removed enum variants for a lot of error states in helix endpoint responses. Most of these are returned by `HelixRequest_Error::Error`
32+
3133
## [v0.5.0] - 2021-05-08
3234

3335
[Commits](https://github.com/Emilgardis/twitch_api2/compare/v0.4.1...v0.5.0)

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ required-features = ["surf_client", "helix"]
104104
[[example]]
105105
name = "get_channel_status"
106106
path = "examples/get_channel_status.rs"
107-
required-features = ["reqwest_client", "helix", "tmi"]
107+
required-features = ["reqwest_client", "helix"]
108108

109109
[[example]]
110110
name = "get_hosts"
@@ -119,7 +119,7 @@ required-features = ["surf_client", "helix"]
119119
[[example]]
120120
name = "get_streams_and_chatters"
121121
path = "examples/get_streams_and_chatters.rs"
122-
required-features = ["surf_client", "tmi"]
122+
required-features = ["surf_client", "helix", "tmi"]
123123

124124
[[example]]
125125
name = "modify_channel"

examples/automod_check.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ async fn run() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>>
3131

3232
let broadcaster_id = token.user_id.as_str();
3333

34-
let client = HelixClient::with_client(surf::Client::new());
34+
let client: HelixClient<surf::Client> = HelixClient::new();
3535

3636
let req = twitch_api2::helix::moderation::CheckAutoModStatusRequest::builder()
3737
.broadcaster_id(broadcaster_id)

examples/channel_information.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ async fn run() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>>
3131
.await
3232
.unwrap();
3333

34-
let client: HelixClient<'static, reqwest::Client> = HelixClient::new();
34+
let client: HelixClient<reqwest::Client> = HelixClient::new();
3535

3636
let user = client
3737
.get_user_from_login(args.next().unwrap(), &token)

examples/create_follower.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ async fn run() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>>
3737
.user_id
3838
.unwrap();
3939

40-
let client = HelixClient::with_client(surf::Client::new());
40+
let client: HelixClient<reqwest::Client> = HelixClient::new();
41+
4142
for user in args {
4243
let user_id = match client
4344
.req_get(

examples/followed_streams.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ async fn run() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>>
3030
.await
3131
.unwrap();
3232

33-
let client: HelixClient<'static, reqwest::Client> = HelixClient::new();
33+
let client: HelixClient<reqwest::Client> = HelixClient::new();
3434

3535
let streams = client.get_followed_streams(&token).await?;
3636
let games = client

examples/get_channel_status.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ async fn run() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>>
3030
.await
3131
.unwrap();
3232

33-
let client: HelixClient<'static, reqwest::Client> = HelixClient::new();
33+
let client: HelixClient<reqwest::Client> = HelixClient::new();
3434

3535
let req = GetStreamsRequest::builder()
3636
.user_login(vec![args.next().unwrap().into()])

examples/get_hosts.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ async fn main() {
1818
return;
1919
};
2020

21-
let client = TmiClient::<'_, surf::Client>::new();
21+
let client: TmiClient<surf::Client> = TmiClient::new();
2222

2323
let response = client
2424
.get_hosts(true, HostsRequestId::Host(channel_id))

examples/get_moderation.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ async fn run() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>>
3434

3535
let broadcaster_id = token.user_id.as_str();
3636

37-
let client = HelixClient::with_client(surf::Client::new());
37+
let client: HelixClient<surf::Client> = HelixClient::new();
3838

3939
println!("====Moderators====");
4040
println!(

examples/get_streams_and_chatters.rs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
use twitch_api2::TmiClient;
2-
use twitch_api2::{helix::streams::GetStreamsRequest, HelixClient};
1+
use twitch_api2::helix::streams::GetStreamsRequest;
2+
use twitch_api2::TwitchClient;
33
use twitch_oauth2::{AccessToken, UserToken};
44

55
#[tokio::main]
@@ -19,23 +19,21 @@ async fn main() {
1919
.await
2020
.unwrap();
2121

22-
let client = reqwest::Client::new();
23-
let client_tmi = TmiClient::with_client(client.clone());
24-
let client_helix = HelixClient::with_client(client);
22+
let client: TwitchClient<surf::Client> = TwitchClient::new();
2523

2624
let streams: Vec<String> = args.collect();
2725
let req = GetStreamsRequest::builder().build();
2826

29-
let response = client_helix.req_get(req, &token).await.unwrap();
27+
let response = client.helix.req_get(req, &token).await.unwrap();
3028

3129
// Note: This will fetch chatters in the current most viewed stream, might spam your console a bit.
3230
println!("GetStreams:\n\t{:?}", response.data);
3331
if let Some(stream) = streams.get(0) {
3432
println!(
3533
"{:?}",
36-
client_tmi.get_chatters(stream.as_str().into()).await
34+
client.tmi.get_chatters(stream.as_str().into()).await
3735
);
3836
} else if let Some(stream) = response.data.get(0).map(|stream| &stream.user_login) {
39-
println!("{:?}", client_tmi.get_chatters(&stream).await);
37+
println!("{:?}", client.tmi.get_chatters(&stream).await);
4038
}
4139
}

examples/modify_channel.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ async fn run() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>>
3535
.user_id
3636
.unwrap();
3737

38-
let client = HelixClient::with_client(surf::Client::new());
38+
let client: HelixClient<surf::Client> = HelixClient::new();
3939

4040
let req = twitch_api2::helix::channels::ModifyChannelInformationRequest::builder()
4141
.broadcaster_id(&*broadcaster_id)

src/client.rs

Lines changed: 159 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,14 @@
5454
//! See the source of this module for the implementation of [`Client`] for [surf](https://crates.io/crates/surf) and [reqwest](https://crates.io/crates/reqwest) if you need inspiration.
5555
//!
5656
57+
use std::convert::TryInto;
5758
use std::error::Error;
5859
use std::future::Future;
60+
use std::str::FromStr;
61+
62+
/// The User-Agent `product` of this crate.
63+
pub static TWITCH_API2_USER_AGENT: &str =
64+
concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
5965

6066
/// A boxed future, mimics `futures::future::BoxFuture`
6167
pub type BoxedFuture<'a, T> = std::pin::Pin<Box<dyn Future<Output = T> + Send + 'a>>;
@@ -72,6 +78,30 @@ pub trait Client<'a>: Send + 'a {
7278
fn req(&'a self, request: Req) -> BoxedFuture<'a, Result<Response, <Self as Client>::Error>>;
7379
}
7480

81+
/// A specific client default for setting some sane defaults for API calls and oauth2 usage
82+
pub trait ClientDefault<'a>: Clone + Sized {
83+
/// Errors that can happen when assembling the client
84+
type Error: std::error::Error + Send + Sync + 'static;
85+
/// Construct [`Self`] with sane defaults for API calls and oauth2.
86+
fn default_client() -> Self {
87+
Self::default_client_with_name(None)
88+
.expect("a new twitch_api2 client without an extra product should never fail")
89+
}
90+
91+
/// Constructs [`Self`] with sane defaults for API calls and oauth2 and setting user-agent to include another product
92+
///
93+
/// Specifically, one should
94+
///
95+
/// * Set User-Agent to `{product} twitch_api2/{version_of_twitch_api2}` (According to RFC7231)
96+
/// See [`TWITCH_API2_USER_AGENT`] for the product of this crate
97+
/// * Disallow redirects
98+
///
99+
/// # Notes
100+
///
101+
/// When the product name is none, this function should never fail. This should be ensured with tests.
102+
fn default_client_with_name(product: Option<http::HeaderValue>) -> Result<Self, Self::Error>;
103+
}
104+
75105
// This makes errors very muddy, preferably we'd actually use rustc_on_unimplemented, but that is highly not recommended (and doesn't work 100% for me at least)
76106
// impl<'a, F, R, E> Client<'a> for F
77107
// where
@@ -86,10 +116,10 @@ pub trait Client<'a>: Send + 'a {
86116
// }
87117
// }
88118

89-
#[cfg(all(feature = "reqwest", feature = "client"))]
119+
#[cfg(feature = "reqwest")]
90120
use reqwest::Client as ReqwestClient;
91121

92-
#[cfg(all(feature = "reqwest", feature = "client"))]
122+
#[cfg(feature = "reqwest")]
93123
#[cfg_attr(nightly, doc(cfg(feature = "reqwest_client")))] // FIXME: This doc_cfg does nothing
94124
impl<'a> Client<'a> for ReqwestClient {
95125
type Error = reqwest::Error;
@@ -120,8 +150,40 @@ impl<'a> Client<'a> for ReqwestClient {
120150
}
121151
}
122152

153+
/// Possible errors from [`ClientDefault::default_client_with_name`] for [reqwest](https://crates.io/crates/reqwest)
154+
#[cfg(feature = "reqwest")]
155+
#[derive(Debug, displaydoc::Display, thiserror::Error)]
156+
pub enum ReqwestClientDefaultError {
157+
/// could not construct header value for User-Agent
158+
InvalidHeaderValue(#[from] http::header::InvalidHeaderValue),
159+
/// reqwest returned an error
160+
ReqwestError(#[from] reqwest::Error),
161+
}
162+
163+
#[cfg(feature = "reqwest")]
164+
impl ClientDefault<'static> for ReqwestClient {
165+
type Error = ReqwestClientDefaultError;
166+
167+
fn default_client_with_name(product: Option<http::HeaderValue>) -> Result<Self, Self::Error> {
168+
let builder = Self::builder();
169+
let user_agent = if let Some(product) = product {
170+
let mut user_agent = product.as_bytes().to_owned();
171+
user_agent.push(b' ');
172+
user_agent.extend(TWITCH_API2_USER_AGENT.as_bytes());
173+
user_agent.as_slice().try_into()?
174+
} else {
175+
http::HeaderValue::from_str(TWITCH_API2_USER_AGENT)?
176+
};
177+
let builder = builder.user_agent(user_agent);
178+
let builder = builder.redirect(reqwest::redirect::Policy::none());
179+
builder.build().map_err(Into::into)
180+
}
181+
}
182+
123183
/// Possible errors from [`Client::req()`] when using the [surf](https://crates.io/crates/surf) client
124-
#[cfg(all(feature = "surf", feature = "client"))]
184+
///
185+
/// Also returned by [`ClientDefault::default_client_with_name`]
186+
#[cfg(feature = "surf")]
125187
#[derive(Debug, displaydoc::Display, thiserror::Error)]
126188
pub enum SurfError {
127189
/// surf failed to do the request: {0}
@@ -134,10 +196,10 @@ pub enum SurfError {
134196
UrlError(#[from] url::ParseError),
135197
}
136198

137-
#[cfg(all(feature = "surf", feature = "client"))]
199+
#[cfg(feature = "surf")]
138200
use surf::Client as SurfClient;
139201

140-
#[cfg(all(feature = "surf", feature = "client"))]
202+
#[cfg(feature = "surf")]
141203
#[cfg_attr(nightly, doc(cfg(feature = "surf_client")))] // FIXME: This doc_cfg does nothing
142204
impl<'a> Client<'a> for SurfClient {
143205
type Error = SurfError;
@@ -206,6 +268,69 @@ impl<'a> Client<'a> for SurfClient {
206268
}
207269
}
208270

271+
/// Possible errors from [`ClientDefault::default_client_with_name`] for [surf](https://crates.io/crates/surf)
272+
#[cfg(feature = "surf")]
273+
#[derive(Debug, displaydoc::Display, thiserror::Error)]
274+
pub enum SurfClientDefaultError {
275+
/// surf returned an error: {0}
276+
SurfError(surf::Error),
277+
}
278+
279+
#[cfg(feature = "surf")]
280+
impl ClientDefault<'static> for SurfClient
281+
where Self: Default
282+
{
283+
type Error = SurfClientDefaultError;
284+
285+
fn default_client_with_name(product: Option<http::HeaderValue>) -> Result<Self, Self::Error> {
286+
#[cfg(feature = "surf")]
287+
struct SurfAgentMiddleware {
288+
user_agent: surf::http::headers::HeaderValue,
289+
}
290+
291+
#[cfg(feature = "surf")]
292+
#[async_trait::async_trait]
293+
impl surf::middleware::Middleware for SurfAgentMiddleware {
294+
async fn handle(
295+
&self,
296+
req: surf::Request,
297+
client: SurfClient,
298+
next: surf::middleware::Next<'_>,
299+
) -> surf::Result<surf::Response> {
300+
let mut req = req;
301+
// if let Some(header) = req.header_mut(surf::http::headers::USER_AGENT) {
302+
// let mut user_agent = self.user_agent.as_str().as_bytes().to_owned();
303+
// user_agent.push(b' ');
304+
// user_agent.extend(header.as_str().as_bytes());
305+
// req.set_header(
306+
// surf::http::headers::USER_AGENT,
307+
// surf::http::headers::HeaderValue::from_bytes(user_agent).expect(
308+
// "product User-Agent + existing User-Agent is expected to be valid ASCII",
309+
// ),
310+
// );
311+
// } else {
312+
req.set_header(surf::http::headers::USER_AGENT, self.user_agent.clone());
313+
// }
314+
next.run(req, client).await
315+
}
316+
}
317+
318+
let client = surf::Client::default();
319+
let user_agent = if let Some(product) = product {
320+
let mut user_agent = product.as_bytes().to_owned();
321+
user_agent.push(b' ');
322+
user_agent.extend(TWITCH_API2_USER_AGENT.as_bytes());
323+
surf::http::headers::HeaderValue::from_bytes(user_agent)
324+
.map_err(SurfClientDefaultError::SurfError)?
325+
} else {
326+
surf::http::headers::HeaderValue::from_str(TWITCH_API2_USER_AGENT)
327+
.map_err(SurfClientDefaultError::SurfError)?
328+
};
329+
let middleware = SurfAgentMiddleware { user_agent };
330+
Ok(client.with(middleware))
331+
}
332+
}
333+
209334
#[derive(Debug, Default, thiserror::Error, Clone)]
210335
/// A client that will never work, used to trick documentation tests
211336
#[error("this client does not do anything, only used for documentation test that only checks")]
@@ -218,3 +343,32 @@ impl<'a> Client<'a> for DummyHttpClient {
218343
Box::pin(async { Err(DummyHttpClient) })
219344
}
220345
}
346+
347+
#[cfg(feature = "surf")]
348+
impl ClientDefault<'static> for DummyHttpClient
349+
where Self: Default
350+
{
351+
type Error = DummyHttpClient;
352+
353+
fn default_client_with_name(_: Option<http::HeaderValue>) -> Result<Self, Self::Error> {
354+
Ok(Self)
355+
}
356+
}
357+
358+
#[cfg(test)]
359+
mod tests {
360+
use super::*;
361+
#[test]
362+
#[cfg(feature = "surf_client")]
363+
fn surf() {
364+
SurfClient::default_client_with_name(Some("test/123".try_into().unwrap())).unwrap();
365+
SurfClient::default_client();
366+
}
367+
368+
#[test]
369+
#[cfg(feature = "reqwest_client")]
370+
fn reqwest() {
371+
ReqwestClient::default_client_with_name(Some("test/123".try_into().unwrap())).unwrap();
372+
ReqwestClient::default_client();
373+
}
374+
}

src/helix/mod.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,8 @@ impl<'a, C: crate::HttpClient<'a>> HelixClient<'a, C> {
135135

136136
/// Create a new [`HelixClient`] with a default [`HttpClient`][crate::HttpClient]
137137
pub fn new() -> HelixClient<'a, C>
138-
where C: Default {
139-
let client = C::default();
138+
where C: crate::client::ClientDefault<'a> {
139+
let client = C::default_client();
140140
HelixClient::with_client(client)
141141
}
142142

@@ -278,10 +278,10 @@ impl<'a, C: crate::HttpClient<'a>> HelixClient<'a, C> {
278278
}
279279

280280
#[cfg(feature = "client")]
281-
impl<'a, C> Default for HelixClient<'a, C>
282-
where C: crate::HttpClient<'a> + Default
281+
impl<C: crate::HttpClient<'static> + crate::client::ClientDefault<'static>> Default
282+
for HelixClient<'static, C>
283283
{
284-
fn default() -> HelixClient<'a, C> { HelixClient::new() }
284+
fn default() -> Self { Self::new() }
285285
}
286286

287287
/// Deserialize "" as <T as Default>::Default

0 commit comments

Comments
 (0)