irelia_cli/
rest.rs

1//! Module containing all the data for the rest LCU bindings
2
3pub mod types;
4
5use http_body_util::BodyExt;
6use hyper::Uri;
7use serde::de::DeserializeOwned;
8use serde::Serialize;
9
10use crate::{utils::process_info::get_running_client, Error, RequestClient};
11
12/// Struct representing a connection to the LCU
13pub struct LcuClient {
14    url: String,
15    auth_header: String,
16}
17
18#[cfg(feature = "batched")]
19pub mod batch {
20    use crate::rest::LcuClient;
21    use crate::{Error, RequestClient};
22    use futures_util::StreamExt;
23    use serde::de::DeserializeOwned;
24    use std::borrow::Cow;
25
26    /// Enum representing the different requests that can be sent to the LCU
27    pub enum RequestType<'a> {
28        Delete,
29        Get,
30        Patch(Option<&'a dyn erased_serde::Serialize>),
31        Post(Option<&'a dyn erased_serde::Serialize>),
32        Put(Option<&'a dyn erased_serde::Serialize>),
33    }
34
35    /// Struct representing a batched request, taking the
36    /// request type and endpoint
37    pub struct Request<'a> {
38        pub request_type: RequestType<'a>,
39        pub endpoint: Cow<'static, str>,
40    }
41
42    impl<'a> Request<'a> {
43        /// Creates a new batched request, which can be wrapped in a slice and send to the LCU
44        pub fn new(request_type: RequestType<'a>, endpoint: impl Into<Cow<'static, str>>) -> Self {
45            Request {
46                request_type,
47                endpoint: endpoint.into(),
48            }
49        }
50
51        pub fn delete(endpoint: impl Into<Cow<'static, str>>) -> Self {
52            Self::new(RequestType::Delete, endpoint)
53        }
54
55        pub fn get(endpoint: impl Into<Cow<'static, str>>) -> Self {
56            Self::new(RequestType::Get, endpoint)
57        }
58
59        pub fn patch(
60            endpoint: impl Into<Cow<'static, str>>,
61            body: Option<&'a dyn erased_serde::Serialize>,
62        ) -> Self {
63            Self::new(RequestType::Patch(body), endpoint)
64        }
65
66        pub fn put(
67            endpoint: impl Into<Cow<'static, str>>,
68            body: Option<&'a dyn erased_serde::Serialize>,
69        ) -> Self {
70            Self::new(RequestType::Put(body), endpoint)
71        }
72
73        pub fn post(
74            endpoint: impl Into<Cow<'static, str>>,
75            body: Option<&'a dyn erased_serde::Serialize>,
76        ) -> Self {
77            Self::new(RequestType::Post(body), endpoint)
78        }
79    }
80
81    impl LcuClient {
82        /// System for batching requests to the LCU by sending a slice
83        /// The buffer size is how many requests can be operated on at
84        /// the same time, returns a vector with all the replies
85        ///
86        /// # Errors
87        /// The value will be an error if the provided type is invalid, or the LCU API is not running
88        pub async fn batched<'a, R>(
89            &self,
90            requests: &[Request<'a>],
91            buffer_size: usize,
92            request_client: &RequestClient,
93        ) -> Vec<Result<Option<R>, Error>>
94        where
95            R: DeserializeOwned,
96        {
97            futures_util::stream::iter(requests.iter().map(|request| async {
98                let endpoint = &*request.endpoint;
99                match &request.request_type {
100                    RequestType::Delete => self.delete(endpoint, request_client).await,
101                    RequestType::Get => self.get(endpoint, request_client).await,
102                    RequestType::Patch(body) => self.patch(endpoint, *body, request_client).await,
103                    RequestType::Post(body) => self.post(endpoint, *body, request_client).await,
104                    RequestType::Put(body) => self.put(endpoint, *body, request_client).await,
105                }
106            }))
107            .buffered(buffer_size)
108            .collect()
109            .await
110        }
111    }
112
113    pub struct Builder;
114
115    mod hidden {
116        use crate::rest::batch::Request;
117        use crate::rest::LcuClient;
118        use crate::RequestClient;
119
120        pub struct WithClient<'a> {
121            pub(super) request_client: &'a RequestClient,
122            pub(super) requests: Vec<Request<'a>>,
123        }
124
125        pub struct WithBufferSize<'a> {
126            pub(super) request_client: &'a RequestClient,
127            pub(super) requests: Vec<Request<'a>>,
128            pub(super) buffer_size: usize,
129        }
130
131        pub struct WithLcuClient<'a> {
132            pub(super) request_client: &'a RequestClient,
133            pub(super) requests: Vec<Request<'a>>,
134            pub(super) buffer_size: usize,
135            pub(super) lcu_client: &'a LcuClient,
136        }
137    }
138
139    use crate::rest::batch::hidden::WithLcuClient;
140    use hidden::{WithBufferSize, WithClient};
141
142    impl Builder {
143        #[must_use]
144        pub fn new() -> Self {
145            Self
146        }
147
148        #[must_use]
149        pub fn with_client(self, request_client: &RequestClient) -> WithClient {
150            WithClient {
151                request_client,
152                requests: Vec::new(),
153            }
154        }
155
156        #[must_use]
157        pub fn with_client_and_capacity(
158            self,
159            request_client: &RequestClient,
160            capacity: usize,
161        ) -> WithClient {
162            WithClient {
163                request_client,
164                requests: Vec::with_capacity(capacity),
165            }
166        }
167    }
168
169    impl Default for Builder {
170        fn default() -> Self {
171            Self
172        }
173    }
174
175    impl<'a> WithClient<'a> {
176        pub fn request(mut self, request: Request<'a>) -> Self {
177            self.add_request(request);
178
179            self
180        }
181
182        pub fn add_request(&mut self, request: Request<'a>) {
183            self.requests.push(request);
184        }
185
186        pub fn with_buffer_size(self, buffer_size: usize) -> WithBufferSize<'a> {
187            WithBufferSize {
188                requests: self.requests,
189                request_client: self.request_client,
190                buffer_size,
191            }
192        }
193    }
194
195    impl<'a> WithBufferSize<'a> {
196        pub fn with_lcu_client(self, lcu_client: &'a LcuClient) -> WithLcuClient<'a> {
197            WithLcuClient {
198                requests: self.requests,
199                request_client: self.request_client,
200                buffer_size: self.buffer_size,
201                lcu_client,
202            }
203        }
204    }
205
206    impl<'a> WithLcuClient<'a> {
207        pub async fn execute<R: DeserializeOwned>(self) -> Vec<Result<Option<R>, Error>> {
208            self.lcu_client
209                .batched(&self.requests, self.buffer_size, self.request_client)
210                .await
211        }
212    }
213}
214
215impl LcuClient {
216    /// Attempts to create a connection to the LCU, errors if it fails
217    /// to spin up the child process, or fails to get data from the client.
218    ///
219    /// force_lock_file will read the lock file regardless of whether the client
220    /// or the game is running at the time
221    /// 
222    /// # Errors
223    /// This will return an error if the LCU API is not running, this can include
224    /// the client being down, the lock file being unable to be opened, or the LCU
225    /// not running at all
226    pub fn new(force_lock_file: bool) -> Result<Self, Error> {
227        let (port, pass) = get_running_client(force_lock_file)?;
228
229        Ok(LcuClient {
230            url: port,
231            auth_header: pass,
232        })
233    }
234
235    #[must_use]
236    /// Creates a new LCU Client that implicitly trusts the port and auth string given,
237    /// Encoding them in a URL and header respectively
238    pub fn new_with_credentials(auth: &str, port: u16) -> LcuClient {
239        LcuClient {
240            url: format!("127.0.0.1:{port}"),
241            auth_header: format!(
242                "Basic {}",
243                crate::utils::process_info::ENCODER.encode(format!("riot:{auth}"))
244            ),
245        }
246    }
247
248    /// Queries the client or lock file, getting a new url and auth header
249    ///
250    /// # Errors
251    /// This will return an error if the lock file is inaccessible, or if
252    /// the LCU is not running
253    pub fn reconnect(&mut self, force_lock_file: bool) -> Result<(), Error> {
254        let (port, pass) = get_running_client(force_lock_file)?;
255        self.url = port;
256        self.auth_header = pass;
257        Ok(())
258    }
259
260    /// Sets the url and auth header according to the auth and port provided
261    pub fn reconnect_with_credentials(&mut self, auth: &str, port: u16) {
262        let port = format!("127.0.0.1:{port}");
263        let pass = format!(
264            "Basic {}",
265            crate::utils::process_info::ENCODER.encode(format!("riot:{auth}"))
266        );
267        self.url = port;
268        self.auth_header = pass;
269    }
270
271    #[must_use]
272    /// Returns a reference to the URL in use
273    pub fn url(&self) -> &str {
274        &self.url
275    }
276
277    #[must_use]
278    /// Returns a reference to the auth header in use
279    pub fn auth_header(&self) -> &str {
280        &self.auth_header
281    }
282
283    /// Sends a delete request to the LCU
284    ///
285    /// # Errors
286    /// This will return an error if the LCU API is not running, or the provided type is invalid
287    pub async fn delete<R: DeserializeOwned>(
288        &self,
289        endpoint: impl AsRef<str>,
290        request_client: &RequestClient,
291    ) -> Result<Option<R>, Error> {
292        self.lcu_request::<(), R>(endpoint.as_ref(), "DELETE", None, request_client)
293            .await
294    }
295
296    /// Sends a get request to the LCU
297    /// ```
298    /// let request_client = irelia::RequestClient::new();
299    /// let lcu_client = irelia::rest::LcuClient::new(false)?;
300    ///
301    ///  let response: Option<serde_json::Value> = lcu_client.get("/example/endpoint/", &request_client)?;
302    /// ```
303    ///
304    /// # Errors
305    /// This will return an error if the LCU API is not running, or the provided type is invalid
306    pub async fn get<R: DeserializeOwned>(
307        &self,
308        endpoint: impl AsRef<str>,
309        request_client: &RequestClient,
310    ) -> Result<Option<R>, Error> {
311        self.lcu_request::<(), R>(endpoint.as_ref(), "GET", None, request_client)
312            .await
313    }
314    
315    /// Sends a head request to the LCU
316    ///
317    /// # Errors
318    /// This will return an error if the LCU API is not running
319    pub async fn head<S>(
320        &self,
321        endpoint: impl AsRef<str>,
322        request_client: &RequestClient,
323    ) -> Result<hyper::Response<hyper::body::Incoming>, Error>
324    {
325        request_client.raw_request_template::<()>(&self.url, endpoint.as_ref(), "HEAD", None, Some(&self.auth_header))
326            .await
327    }
328
329    /// Sends a patch request to the LCU
330    ///
331    /// # Errors
332    /// This will return an error if the LCU API is not running, or the provided type or body is invalid
333    pub async fn patch<T, R>(
334        &self,
335        endpoint: impl AsRef<str>,
336        body: Option<T>,
337        request_client: &RequestClient,
338    ) -> Result<Option<R>, Error>
339    where
340        T: Serialize,
341        R: DeserializeOwned,
342    {
343        self.lcu_request(endpoint.as_ref(), "PATCH", body, request_client)
344            .await
345    }
346
347    /// Sends a post request to the LCU
348    ///
349    /// # Errors
350    /// This will return an error if the LCU API is not running, or the provided type or body is invalid
351    pub async fn post<T, R>(
352        &self,
353        endpoint: impl AsRef<str>,
354        body: Option<T>,
355        request_client: &RequestClient,
356    ) -> Result<Option<R>, Error>
357    where
358        T: Serialize,
359        R: DeserializeOwned,
360    {
361        self.lcu_request(endpoint.as_ref(), "POST", body, request_client)
362            .await
363    }
364
365    /// Sends a put request to the LCU
366    ///
367    /// # Errors
368    /// This will return an error if the LCU API is not running, or the provided type or body is invalid
369    pub async fn put<T, R>(
370        &self,
371        endpoint: impl AsRef<str>,
372        body: Option<T>,
373        request_client: &RequestClient,
374    ) -> Result<Option<R>, Error>
375    where
376        T: Serialize,
377        R: DeserializeOwned,
378    {
379        self.lcu_request(endpoint.as_ref(), "PUT", body, request_client)
380            .await
381    }
382
383    /// Fetches the schema from a remote endpoint, for example:
384    /// <`https://raw.githubusercontent.com/dysolix/hasagi-types/main/swagger.json/`>
385    ///
386    /// # Errors
387    ///
388    /// This function will error if it fails to connect to the given remote,
389    /// or if the given remote cannot be deserialized to match the `Schema` type
390    pub async fn schema(remote: &'static str) -> Result<types::Schema, Error> {
391        let uri = Uri::from_static(remote);
392        // This creates a custom client, as the default hyper client used by Irelia needs a cert, and it has no use outside here
393        let https = hyper_rustls::HttpsConnectorBuilder::new()
394            .with_native_roots()
395            .map_err(Error::StdIo)?
396            .https_only()
397            .enable_http1()
398            .build();
399        let client =
400            hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new())
401                .build::<_, http_body_util::Full<hyper::body::Bytes>>(https);
402        let mut request = client.get(uri).await.map_err(Error::HyperClientError)?;
403        let tmp = request.body_mut();
404        let body = tmp.collect().await.map_err(Error::HyperError)?.to_bytes();
405        serde_json::from_slice(&body).map_err(Error::SerdeJsonError)
406    }
407
408    /// Makes a request to the LCU with an unspecified method, valid options being
409    /// "PUT", "GET", "POST", "HEAD", "DELETE"
410    ///
411    /// # Errors
412    /// This will return an error if the LCU API is not running, or the provided type or body is invalid
413    pub async fn lcu_request<T: Serialize, R: DeserializeOwned>(
414        &self,
415        endpoint: &str,
416        method: &str,
417        body: Option<T>,
418        request_client: &RequestClient,
419    ) -> Result<Option<R>, Error> {
420        request_client
421            .request_template(
422                &self.url,
423                endpoint,
424                method,
425                body,
426                Some(&self.auth_header),
427                |bytes| {
428                    let body = if bytes.is_empty() {
429                        None
430                    } else {
431                        Some(serde_json::from_slice(&bytes)?)
432                    };
433
434                    Ok(body)
435                },
436            )
437            .await
438    }
439}
440
441#[cfg(feature = "batched")]
442#[cfg(test)]
443mod tests {
444    use crate::{rest::LcuClient, RequestClient};
445
446    #[tokio::test]
447    async fn batch_test() {
448        use crate::rest::{
449            batch::{Request, RequestType},
450            LcuClient,
451        };
452
453        let page = serde_json::json!(
454            {
455              "blocks": [
456                {
457                  "items": [
458                    {
459                      "count": 1,
460                      "id": "3153"
461                    },
462                  ],
463                  "type": "Final Build"
464                }
465              ],
466              "title": "Test Build",
467            }
468        );
469        let client = RequestClient::new();
470
471        let lcu_client = LcuClient::new(false).unwrap();
472
473        let request: serde_json::Value = lcu_client
474            .get("/lol-summoner/v1/current-summoner", &client)
475            .await
476            .unwrap()
477            .unwrap();
478
479        let id = &request["summonerId"];
480
481        let endpoint = format!("/lol-item-sets/v1/item-sets/{id}/sets");
482
483        let mut json: serde_json::Value = lcu_client
484            .get(endpoint.as_str(), &client)
485            .await
486            .unwrap()
487            .unwrap();
488
489        json["itemSets"].as_array_mut().unwrap().push(page);
490
491        let req = Request {
492            request_type: RequestType::Put(Some(&json)),
493            endpoint: format!("/lol-item-sets/v1/item-sets/{id}/sets").into(),
494        };
495
496        let result = lcu_client
497            .batched::<serde_json::Value>(&[req], 1, &client)
498            .await;
499
500        println!("{result:?}");
501
502        let a = lcu_client
503            .put::<_, serde_json::Value>(
504                format!("/lol-item-sets/v1/item-sets/{id}/sets"),
505                Some(json),
506                &client,
507            )
508            .await;
509        println!("{a:?}");
510    }
511
512    #[tokio::test]
513    async fn test_schema_des() {
514        let _schema = LcuClient::schema(
515            "https://raw.githubusercontent.com/dysolix/hasagi-types/main/swagger.json",
516        )
517        .await
518        .unwrap();
519    }
520}