glimesh/conn/
http.rs

1//! Connection over http using reqwest.
2//!
3//! You will usually pretty much immediately turn the connection into a Client.
4//! E.g.
5//! ```rust
6//! use glimesh::{http::Connection, Auth};
7//! let client = Connection::new(Auth::client_id("<GLIMESH_CLIENT_ID>")).into_client();
8//! ```
9
10use crate::{Auth, Client, GlimeshError, HttpConnectionError, MutationConn, QueryConn};
11use reqwest::{header, RequestBuilder};
12use std::{sync::Arc, time::Duration};
13
14#[derive(Debug)]
15struct Config {
16    user_agent: String,
17    timeout: Duration,
18    api_url: String,
19    auth: Option<Auth>,
20}
21
22impl Default for Config {
23    fn default() -> Self {
24        Config {
25            user_agent: format!("Glimesh Rust / {}", env!("CARGO_PKG_VERSION")),
26            timeout: Duration::from_secs(30),
27            api_url: String::from("https://glimesh.tv/api/graph"),
28            auth: None,
29        }
30    }
31}
32
33/// Configure and build an http [`Connection`].
34///
35/// ## Usage:
36/// ```rust
37/// let connection = Connection::builder()
38///     .user_agent("My App / 0.1.0")
39///     .finish();
40/// ```
41#[derive(Debug, Default)]
42pub struct ConnectionBuilder {
43    config: Config,
44    http: Option<reqwest::Client>,
45}
46
47impl ConnectionBuilder {
48    /// Finish building the http connection from the set options.
49    ///
50    /// # Panics
51    /// This function panics if (when an http client hasn't already been provided) the TLS backend
52    /// cannot be initialized, or the resolver cannot load the system configuration.
53    pub fn finish(self) -> Connection {
54        Connection {
55            http: self.http.unwrap_or_else(move || {
56                reqwest::Client::builder()
57                    .user_agent(self.config.user_agent)
58                    .timeout(self.config.timeout)
59                    .build()
60                    .expect("failed to create http client")
61            }),
62            auth: self.config.auth.map(Arc::new),
63            api_url: Arc::new(self.config.api_url),
64        }
65    }
66
67    /// Set the user agent the http client will identify itself as.
68    ///
69    /// This defaults to `Glimesh Rust / x.x.x` where `x.x.x` is the version of this package.
70    ///
71    /// This has no effect if a custom http client is passed in
72    pub fn user_agent(mut self, value: impl Into<String>) -> Self {
73        self.config.user_agent = value.into();
74        self
75    }
76
77    /// Set the timeout for requests made to glimesh.
78    ///
79    /// The default is 30 seconds
80    ///
81    /// This has no effect if a custom http client is passed in
82    pub fn timeout(mut self, value: Duration) -> Self {
83        self.config.timeout = value;
84        self
85    }
86
87    /// Set the base api url used for request.
88    /// Useful if running Glimesh locally, or using the old api for example.
89    ///
90    /// Defaults to `https://glimesh.tv/api/graph`
91    pub fn api_url(mut self, value: impl Into<String>) -> Self {
92        self.config.api_url = value.into();
93        self
94    }
95
96    /// Set the auth method used in requests.
97    pub fn auth(mut self, auth: Auth) -> Self {
98        self.config.auth = Some(auth);
99        self
100    }
101
102    /// Use an existing http client.
103    pub fn http_client(mut self, http: reqwest::Client) -> Self {
104        self.http = Some(http);
105        self
106    }
107}
108
109/// Connect to glimesh over http(s).
110#[derive(Debug, Clone)]
111pub struct Connection {
112    http: reqwest::Client,
113    auth: Option<Arc<Auth>>,
114    api_url: Arc<String>,
115}
116
117impl Connection {
118    /// Create a [`ConnectionBuilder`] to configure various options.
119    pub fn builder() -> ConnectionBuilder {
120        ConnectionBuilder::default()
121    }
122
123    /// Create a connection with the default options.
124    pub fn new(auth: Auth) -> Self {
125        ConnectionBuilder::default().auth(auth).finish()
126    }
127
128    /// Create a client with reference to this connection
129    pub fn as_client(&self) -> Client<&Self> {
130        Client::new(self)
131    }
132
133    /// Create a client with a clone of this connection
134    pub fn to_client(&self) -> HttpClient {
135        Client::new(self.clone())
136    }
137
138    /// Convert this connection into a client
139    pub fn into_client(self) -> HttpClient {
140        Client::new(self)
141    }
142
143    /// Create a copy of this connection with a difference auth method.
144    pub fn clone_with_auth(&self, auth: Auth) -> Self {
145        Self {
146            api_url: self.api_url.clone(),
147            http: self.http.clone(),
148            auth: Some(Arc::new(auth)),
149        }
150    }
151
152    async fn request<Q>(
153        &self,
154        variables: Q::Variables,
155    ) -> Result<Q::ResponseData, HttpConnectionError>
156    where
157        Q: graphql_client::GraphQLQuery,
158    {
159        let req = self
160            .http
161            .post(self.api_url.as_ref())
162            .json(&Q::build_query(variables));
163
164        let res = self
165            .apply_auth(req)
166            .await?
167            .send()
168            .await
169            .map_err(anyhow::Error::from)?;
170
171        if !res.status().is_success() {
172            return Err(HttpConnectionError::BadStatus(res.status().as_u16()));
173        }
174
175        let res: graphql_client::Response<Q::ResponseData> =
176            res.json().await.map_err(anyhow::Error::from)?;
177
178        if let Some(errs) = res.errors {
179            if !errs.is_empty() {
180                return Err(GlimeshError::GraphqlErrors(errs).into());
181            }
182        }
183
184        let data = res.data.ok_or(GlimeshError::NoData)?;
185        Ok(data)
186    }
187
188    async fn apply_auth(&self, req: RequestBuilder) -> Result<RequestBuilder, HttpConnectionError> {
189        match self.auth.as_ref().map(|a| a.as_ref()) {
190            Some(Auth::ClientId(client_id)) => {
191                Ok(req.header(header::AUTHORIZATION, format!("Client-ID {}", client_id)))
192            }
193            Some(Auth::AccessToken(access_token)) => Ok(req.bearer_auth(access_token)),
194            Some(Auth::RefreshableAccessToken(token)) => {
195                let tokens = token.access_token().await?;
196                Ok(req.bearer_auth(tokens.access_token))
197            }
198            Some(Auth::ClientCredentials(client_credentials)) => {
199                let tokens = client_credentials.access_token().await?;
200                Ok(req.bearer_auth(tokens.access_token))
201            }
202            None => Ok(req),
203        }
204    }
205}
206
207#[async_trait]
208impl QueryConn for Connection {
209    type Error = HttpConnectionError;
210
211    async fn query<Q>(&self, variables: Q::Variables) -> Result<Q::ResponseData, Self::Error>
212    where
213        Q: graphql_client::GraphQLQuery,
214        Q::Variables: Send + Sync,
215    {
216        self.request::<Q>(variables).await
217    }
218}
219
220#[async_trait]
221impl MutationConn for Connection {
222    type Error = HttpConnectionError;
223
224    async fn mutate<Q>(&self, variables: Q::Variables) -> Result<Q::ResponseData, Self::Error>
225    where
226        Q: graphql_client::GraphQLQuery,
227        Q::Variables: Send + Sync,
228    {
229        self.request::<Q>(variables).await
230    }
231}
232
233/// Type alias for a http backed client
234pub type HttpClient = Client<Connection>;