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}