playground_api/
client.rs

1use crate::{endpoints::*, error::Error};
2use serde::{de::Deserialize, Serialize};
3use url::{ParseError, Url};
4
5/// A client for interacting with the Rust playground API.
6///
7/// Holds the base URL and the `reqwest::Client` struct for all requests.
8#[derive(Clone)]
9pub struct Client {
10    url: Url,
11    client: reqwest::Client,
12}
13
14impl Client {
15    /// Creates a new `Client` instance with the given base URL.
16    ///
17    /// Parses the provided string into a `Url`. Returns an error if the URL is invalid.
18    ///
19    /// # Arguments
20    ///
21    /// * `url` - A string slice representing the base URL of the Rust playground API.
22    ///
23    /// # Returns
24    ///
25    /// * `Result<Client, Error>` - On success, returns a `Client` configured with the parsed URL.
26    ///   On failure, returns an `Error` if the URL string is invalid.
27    pub fn new(url: &str) -> Result<Client, Error> {
28        let url = Url::parse(url)?;
29        let client = reqwest::Client::new();
30        Ok(Client { url, client })
31    }
32
33    /// Sends a code execution request to the Rust playground and returns the result.
34    ///
35    /// This asynchronous method takes and [`ExecuteRequest`] struct containing the code
36    /// execution parameters, sends it to the appropriate endpoint on the Rust playground
37    /// via a POST request, and returns the execution result.
38    ///
39    /// # Arguments
40    ///
41    /// * `request` - A reference to an [`ExecuteRequest`] that includes the code snippet
42    ///   and configuration options such as edition, crate type, and whether to run or compile.
43    ///
44    /// # Returns
45    ///
46    /// * `Result<ExecuteResponse, Error>` - On success, returns an [`ExecuteResponse`] containing
47    ///   the output, errors, and status from the Rust playground. On failure, returns an [`Error`].
48    ///
49    /// # Errors
50    ///
51    /// This function will return an error if the HTTP request fails, if the response cannot be parsed,
52    /// or if the playground service is unavailable.
53    pub async fn execute(&self, request: &ExecuteRequest) -> Result<ExecuteResponse, Error> {
54        self.post(request, Endpoints::Execute).await
55    }
56
57    /// Sends a code compilation request to the Rust playground and returns the result.
58    ///
59    /// This asynchronous method takes a [`CompileRequest`] containing the code and
60    /// compilation parameters, sends it to the Rust playground's compile endpoint,
61    /// and returns the compilation result.
62    ///
63    /// # Arguments
64    ///
65    /// * `request` - A reference to a [`CompileRequest`] that includes the code and metadata
66    ///   such as the toolchain edition, crate type, target, and any compiler settings.
67    ///
68    /// # Returns
69    ///
70    /// * `Result<CompileResponse, Error>` - On success, returns a [`CompileResponse`] containing
71    ///   the compiler output, including success/failure status, messages, and possible warnings or errors.
72    ///   On failure, returns an [`Error`] describing the issue.
73    ///
74    /// # Errors
75    ///
76    /// Returns an error if the HTTP request fails, if the response cannot be parsed correctly,
77    /// or if the playground service encounters an issue.
78    pub async fn compile(&self, request: &CompileRequest) -> Result<CompileResponse, Error> {
79        self.post(request, Endpoints::Compile).await
80    }
81
82    /// Sends a code formatting request to the Rust playground and returns the formatted result.
83    ///
84    /// This asynchronous method takes a [`FormatRequest`] containing the Rust code and formatting options,
85    /// sends it to the Rust playground's format endpoint, and returns the formatted code or any errors.
86    ///
87    /// # Arguments
88    ///
89    /// * `request` - A reference to a [`FormatRequest`] that includes the code to be formatted and
90    ///   optional parameters like the edition to use.
91    ///
92    /// # Returns
93    ///
94    /// * `Result<FormatResponse, Error>` - On success, returns a [`FormatResponse`] containing the
95    ///   formatted code or an error message if the code could not be formatted.
96    ///   On failure, returns an [`Error`] representing issues like network failure or parsing problems.
97    ///
98    /// # Errors
99    ///
100    /// This function may return an error if the request fails, the response is invalid,
101    /// or the Rust playground's formatting service encounters a problem.
102    pub async fn format(&self, request: &FormatRequest) -> Result<FormatResponse, Error> {
103        self.post(request, Endpoints::Format).await
104    }
105
106    /// Sends a Clippy linting request to the Rust playground and returns the analysis result.
107    ///
108    /// This asynchronous method takes a [`ClippyRequest`] containing the Rust code and configuration,
109    /// sends it to the Rust playground's Clippy endpoint, and returns any linter warnings, errors,
110    /// or suggestions provided by Clippy.
111    ///
112    /// # Arguments
113    ///
114    /// * `request` - A reference to a [`ClippyRequest`] that includes the code to be analyzed
115    ///   and optional parameters such as edition or crate type.
116    ///
117    /// # Returns
118    ///
119    /// * `Result<ClippyResponse, Error>` - On success, returns a [`ClippyResponse`] containing
120    ///   Clippy's diagnostic output (warnings, errors, suggestions). On failure, returns an [`Error`]
121    ///   describing what went wrong (e.g., network error, bad request, or service issue).
122    ///
123    /// # Errors
124    ///
125    /// Returns an error if the request cannot be completed, the response is invalid,
126    /// or the Clippy service is unavailable or encounters an internal error.
127    pub async fn clippy(&self, request: &ClippyRequest) -> Result<ClippyResponse, Error> {
128        self.post(request, Endpoints::Clippy).await
129    }
130
131    /// Sends a Miri request to the Rust playground and returns the result of interpreting the code.
132    ///
133    /// This asynchronous method takes a [`MiriRequest`] containing the Rust code and any
134    /// interpreter-specific options, sends it to the Rust playground's Miri endpoint, and
135    /// returns the result of running the interpreter on the code.
136    ///
137    /// # Arguments
138    ///
139    /// * `request` - A reference to a [`MiriRequest`] that includes the code and metadata
140    ///   such as edition, crate type, and other configuration options.
141    ///
142    /// # Returns
143    ///
144    /// * `Result<MiriResponse, Error>` - On success, returns a [`MiriResponse`] containing the
145    ///   result of the interpretation. On failure, returns an [`Error`] describing the issue.
146    ///
147    /// # Errors
148    ///
149    /// Returns an error if the request fails, if the response is invalid, or if the Miri service
150    /// encounters an internal issue.
151    pub async fn miri(&self, request: &MiriRequest) -> Result<MiriResponse, Error> {
152        self.post(request, Endpoints::Miri).await
153    }
154
155    /// Sends a macro expansion request to the Rust playground and returns the result.
156    ///
157    /// This asynchronous method takes a [`MacroExpansionRequest`] with Rust code containing macros,
158    /// sends it to the Rust playground's macro expansion endpoint, and returns the result
159    /// of the expanded macros.
160    ///
161    /// # Arguments
162    ///
163    /// * `request` - A reference to a [`MacroExpansionRequest`] that includes the code and any
164    ///   configuration options like the edition to use.
165    ///
166    /// # Returns
167    ///
168    /// * `Result<MacroExpansionResponse, Error>` - On success, returns a [`MacroExpansionResponse`]
169    ///   containing the macro-expanded version of the code. On failure, returns an [`Error`] describing
170    ///   the issue.
171    ///
172    /// # Errors
173    ///
174    /// Returns an error if the HTTP request fails, if the response is invalid, or if the macro expansion
175    /// service encounters an issue.
176    pub async fn macro_expansion(
177        &self,
178        request: &MacroExpansionRequest,
179    ) -> Result<MacroExpansionResponse, Error> {
180        self.post(request, Endpoints::MacroExpansion).await
181    }
182
183    /// Retrieves the list of available crates from the Rust playground.
184    ///
185    /// This asynchronous method sends a GET request to the crates endpoint
186    /// and returns a list of crates supported by the playground environment.
187    ///
188    /// # Returns
189    ///
190    /// * `Result<CratesResponse, Error>` - On success, returns a [`CratesResponse`] containing
191    ///   the names and versions of available crates. On failure, returns an [`Error`] describing
192    ///   the problem.
193    ///
194    /// # Errors
195    ///
196    /// Returns an error if the request fails, if the response cannot be parsed,
197    /// or if the crates service is unavailable.
198    pub async fn crates(&self) -> Result<CratesResponse, Error> {
199        self.get(Endpoints::Crates).await
200    }
201
202    /// Retrieves the supported versions and metadata of the Rust playground.
203    ///
204    /// This asynchronous method sends a GET request to the versions endpoint and
205    /// returns information about supported Rust versions, targets, and environments.
206    ///
207    /// # Returns
208    ///
209    /// * `Result<VersionsResponse, Error>` - On success, returns a [`VersionsResponse`]
210    ///   containing version details. On failure, returns an [`Error`] describing what went wrong.
211    ///
212    /// # Errors
213    ///
214    /// Returns an error if the request cannot be completed, the response is malformed,
215    /// or if the versions service is unavailable.
216    pub async fn versions(&self) -> Result<VersionsResponse, Error> {
217        self.get(Endpoints::Versions).await
218    }
219
220    /// Creates a GitHub Gist from the provided Rust playground code.
221    ///
222    /// This asynchronous method sends a [`GistCreateRequest`] to the Gist creation endpoint
223    /// and returns a response containing the Gist URL or error information.
224    ///
225    /// # Arguments
226    ///
227    /// * `request` - A reference to a [`GistCreateRequest`] that includes the code to be uploaded
228    ///   as a Gist and any additional metadata like description or visibility.
229    ///
230    /// # Returns
231    ///
232    /// * `Result<GistResponse, Error>` - On success, returns a [`GistResponse`] containing
233    ///   the Gist ID and URL. On failure, returns an [`Error`] describing what went wrong.
234    ///
235    /// # Errors
236    ///
237    /// Returns an error if the HTTP request fails, if the response is malformed,
238    /// or if the Gist service is unavailable.
239    pub async fn gist_create(&self, request: &GistCreateRequest) -> Result<GistResponse, Error> {
240        self.post(request, Endpoints::GistCreate).await
241    }
242
243    /// Retrieves an existing GitHub Gist from the Rust playground.
244    ///
245    /// This asynchronous method sends a GET request to the Gist retrieval endpoint
246    /// using the provided Gist ID and returns the contents of the Gist.
247    ///
248    /// # Arguments
249    ///
250    /// * `id` - A `String` representing the unique identifier of the Gist to retrieve.
251    ///
252    /// # Returns
253    ///
254    /// * `Result<GistResponse, Error>` - On success, returns a [`GistResponse`] containing
255    ///   the Gist's code and metadata. On failure, returns an [`Error`] describing the issue.
256    ///
257    /// # Errors
258    ///
259    /// Returns an error if the HTTP request fails, if the response is invalid,
260    /// or if the Gist could not be found.
261    pub async fn gist_get(&self, id: String) -> Result<GistResponse, Error> {
262        self.get(Endpoints::GistGet(id)).await
263    }
264
265    /// Sends a POST request with a serialized JSON payload to the specified endpoint,
266    /// and deserializes the response into the expected type.
267    ///
268    /// Used internally to interact with Rust playground endpoints.
269    async fn post<T, U>(&self, request: &T, endpoint: Endpoints) -> Result<U, Error>
270    where
271        T: Serialize,
272        U: for<'de> Deserialize<'de>,
273    {
274        let url = self.get_url(endpoint)?;
275        let res = self.client.post(url).json(request).send().await?;
276
277        if !res.status().is_success() {
278            return Err(Error::NoSuccess(res.status().as_u16()));
279        }
280
281        let res = res.json::<U>().await?;
282        Ok(res)
283    }
284
285    /// Sends a GET request to the specified endpoint, and deserializes the response
286    /// into the expected type.
287    ///
288    /// Used internally to interact with Rust playground endpoints.
289    async fn get<U>(&self, endpoint: Endpoints) -> Result<U, Error>
290    where
291        U: for<'de> Deserialize<'de>,
292    {
293        let url = self.get_url(endpoint)?;
294        let res = self.client.get(url).send().await?;
295
296        if !res.status().is_success() {
297            return Err(Error::NoSuccess(res.status().as_u16()));
298        }
299
300        let res = res.json::<U>().await?;
301        Ok(res)
302    }
303
304    /// Takes an endpoint and returns the correct url.
305    fn get_url(&self, endpoint: Endpoints) -> Result<Url, ParseError> {
306        let url = match endpoint {
307            Endpoints::Execute => self.url.join("execute"),
308            Endpoints::Compile => self.url.join("compile"),
309            Endpoints::Format => self.url.join("format"),
310            Endpoints::Clippy => self.url.join("clippy"),
311            Endpoints::Miri => self.url.join("miri"),
312            Endpoints::Crates => self.url.join("meta/crates"),
313            Endpoints::Versions => self.url.join("meta/versions"),
314            Endpoints::MacroExpansion => self.url.join("macro-expansion"),
315            Endpoints::GistCreate => self.url.join("meta/gist"),
316            Endpoints::GistGet(id) => self.url.join(&format!("meta/gist/{}", id)),
317        }?;
318        Ok(url)
319    }
320}
321
322impl Default for Client {
323    /// Creates a `Client` instance with the following url <https://play.rust-lang.org/>
324    fn default() -> Self {
325        let client = reqwest::Client::new();
326        Self {
327            url: Url::parse("https://play.rust-lang.org/").unwrap(),
328            client,
329        }
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use super::Client;
336    use crate::endpoints::*;
337
338    #[tokio::test]
339    async fn execute() {
340        let req = ExecuteRequest::default();
341
342        let client = Client::default();
343        let res = client.execute(&req).await;
344
345        println!("{:?}", res);
346        assert!(res.is_ok())
347    }
348
349    #[tokio::test]
350    async fn compile() {
351        let req = CompileRequest::default();
352
353        let client = Client::default();
354        let res = client.compile(&req).await;
355
356        println!("{:?}", res);
357        assert!(res.is_ok());
358    }
359
360    #[tokio::test]
361    async fn format() {
362        let req = FormatRequest::default();
363
364        let client = Client::default();
365        let res = client.format(&req).await;
366
367        println!("{:?}", res);
368        assert!(res.is_ok());
369    }
370
371    #[tokio::test]
372    async fn clippy() {
373        let req = ClippyRequest::default();
374
375        let client = Client::default();
376        let res = client.clippy(&req).await;
377
378        println!("{:?}", res);
379        assert!(res.is_ok());
380    }
381
382    #[tokio::test]
383    async fn miri() {
384        let req = MiriRequest::default();
385
386        let client = Client::default();
387        let res = client.miri(&req).await;
388
389        println!("{:?}", res);
390        assert!(res.is_ok());
391    }
392
393    #[tokio::test]
394    async fn macro_expansion() {
395        let req = MacroExpansionRequest::default();
396
397        let client = Client::default();
398        let res = client.macro_expansion(&req).await;
399
400        println!("{:?}", res);
401        assert!(res.is_ok());
402    }
403
404    #[tokio::test]
405    async fn crates() {
406        let client = Client::default();
407        let res = client.crates().await;
408
409        println!("{:?}", res);
410        assert!(res.is_ok());
411    }
412
413    #[tokio::test]
414    async fn version() {
415        let client = Client::default();
416        let res = client.versions().await;
417
418        println!("{:?}", res);
419        assert!(res.is_ok());
420    }
421
422    #[tokio::test]
423    #[ignore = "don't flood rust playground gist with useless gists"]
424    async fn gist_create() {
425        let req = GistCreateRequest::new("fn main() { println!(\"Hello, world!\"); }".to_owned());
426
427        let client = Client::default();
428        let res = client.gist_create(&req).await;
429
430        println!("{:?}", res);
431        assert!(res.is_ok());
432    }
433
434    #[tokio::test]
435    async fn gist_get() {
436        let id = "ba5e40fb63e78da440797e921bbf2aa6".to_owned();
437
438        let client = Client::default();
439        let res = client.gist_get(id).await;
440
441        println!("{:?}", res);
442        assert!(res.is_ok());
443    }
444}