54
54
//! 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.
55
55
//!
56
56
57
+ use std:: convert:: TryInto ;
57
58
use std:: error:: Error ;
58
59
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" ) , ) ;
59
65
60
66
/// A boxed future, mimics `futures::future::BoxFuture`
61
67
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 {
72
78
fn req ( & ' a self , request : Req ) -> BoxedFuture < ' a , Result < Response , <Self as Client >:: Error > > ;
73
79
}
74
80
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
+
75
105
// 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)
76
106
// impl<'a, F, R, E> Client<'a> for F
77
107
// where
@@ -86,10 +116,10 @@ pub trait Client<'a>: Send + 'a {
86
116
// }
87
117
// }
88
118
89
- #[ cfg( all ( feature = "reqwest" , feature = "client" ) ) ]
119
+ #[ cfg( feature = "reqwest" ) ]
90
120
use reqwest:: Client as ReqwestClient ;
91
121
92
- #[ cfg( all ( feature = "reqwest" , feature = "client" ) ) ]
122
+ #[ cfg( feature = "reqwest" ) ]
93
123
#[ cfg_attr( nightly, doc( cfg( feature = "reqwest_client" ) ) ) ] // FIXME: This doc_cfg does nothing
94
124
impl < ' a > Client < ' a > for ReqwestClient {
95
125
type Error = reqwest:: Error ;
@@ -120,8 +150,40 @@ impl<'a> Client<'a> for ReqwestClient {
120
150
}
121
151
}
122
152
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
+
123
183
/// 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" ) ]
125
187
#[ derive( Debug , displaydoc:: Display , thiserror:: Error ) ]
126
188
pub enum SurfError {
127
189
/// surf failed to do the request: {0}
@@ -134,10 +196,10 @@ pub enum SurfError {
134
196
UrlError ( #[ from] url:: ParseError ) ,
135
197
}
136
198
137
- #[ cfg( all ( feature = "surf" , feature = "client" ) ) ]
199
+ #[ cfg( feature = "surf" ) ]
138
200
use surf:: Client as SurfClient ;
139
201
140
- #[ cfg( all ( feature = "surf" , feature = "client" ) ) ]
202
+ #[ cfg( feature = "surf" ) ]
141
203
#[ cfg_attr( nightly, doc( cfg( feature = "surf_client" ) ) ) ] // FIXME: This doc_cfg does nothing
142
204
impl < ' a > Client < ' a > for SurfClient {
143
205
type Error = SurfError ;
@@ -206,6 +268,69 @@ impl<'a> Client<'a> for SurfClient {
206
268
}
207
269
}
208
270
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
+
209
334
#[ derive( Debug , Default , thiserror:: Error , Clone ) ]
210
335
/// A client that will never work, used to trick documentation tests
211
336
#[ 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 {
218
343
Box :: pin ( async { Err ( DummyHttpClient ) } )
219
344
}
220
345
}
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
+ }
0 commit comments