├── .github
└── workflows
│ ├── ci.yml
│ └── release.yml
├── .gitignore
├── Cargo.toml
├── LICENSE
├── README.md
├── scripts
└── coverage.sh
└── src
├── client.rs
├── executor.rs
└── lib.rs
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 |
9 | jobs:
10 | lint-and-format:
11 | name: Lint and format
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - name: Checkout code
16 | uses: actions/checkout@v2
17 |
18 | - name: Install rust stable
19 | uses: actions-rs/toolchain@v1
20 | with:
21 | toolchain: stable
22 |
23 | - name: Run format check
24 | run: cargo fmt -- --check
25 |
26 | - name: Run clippy check
27 | run: cargo clippy
28 |
29 | check-coverage:
30 | name: Check coverage
31 | runs-on: ubuntu-latest
32 |
33 | steps:
34 | - name: Checkout code
35 | uses: actions/checkout@v2
36 |
37 | - name: Install rust nightly
38 | uses: actions-rs/toolchain@v1
39 | with:
40 | toolchain: nightly
41 | override: true
42 | components: llvm-tools-preview
43 |
44 | - name: Install grcov
45 | run: cargo install grcov
46 |
47 | - name: Run coverage report
48 | run: ./scripts/coverage.sh
49 |
50 | test-and-build:
51 | name: Test and build
52 |
53 | strategy:
54 | fail-fast: false
55 | matrix:
56 | os: [ubuntu-latest, macos-latest, windows-latest]
57 | rust_version: [1.46.0, stable, nightly]
58 |
59 | runs-on: ${{ matrix.os }}
60 |
61 | steps:
62 | - name: Checkout code
63 | uses: actions/checkout@v2
64 |
65 | - name: Install rust version ${{ matrix.rust_version }}
66 | uses: actions-rs/toolchain@v1
67 | with:
68 | toolchain: ${{ matrix.rust_version }}
69 |
70 | - name: Run tests
71 | run: cargo test
72 |
73 | - name: Build package
74 | run: cargo build
75 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | release:
5 | types: [published]
6 |
7 | jobs:
8 | check-version:
9 | name: Check Version
10 |
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - name: Checkout code
15 | uses: actions/checkout@v2
16 |
17 | - name: Check version
18 | run: |
19 | [[ $(grep -m 1 -oP 'version = "(.*)"' Cargo.toml | sed -rn 's/.*"(.*)"/v\1/p') == ${{ github.event.release.tag_name }} ]]
20 |
21 | publish:
22 | name: Publish release
23 |
24 | runs-on: ubuntu-latest
25 | needs: check-version
26 |
27 | steps:
28 | - name: Checkout code
29 | uses: actions/checkout@v2
30 |
31 | - name: Install rust stable
32 | uses: actions-rs/toolchain@v1
33 | with:
34 | toolchain: stable
35 |
36 | - name: Login to crates.io
37 | env:
38 | TOKEN: ${{ secrets.CRATES_IO_TOKEN }}
39 |
40 | run: cargo login $TOKEN
41 |
42 | - name: Publish dry-run
43 | run: cargo publish --dry-run
44 |
45 | - name: Publish
46 | run: cargo publish
47 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | Cargo.lock
3 | .tester
4 | .venv
5 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "piston_rs"
3 | description = "An async wrapper for the Piston code execution engine."
4 | version = "0.4.3"
5 | edition = "2021"
6 | authors = ["Jonxslays"]
7 | readme = "README.md"
8 | license = "MIT"
9 | homepage = "https://github.com/Jonxslays/piston_rs"
10 | repository = "https://github.com/Jonxslays/piston_rs"
11 | documentation = "https://docs.rs/piston_rs"
12 | keywords = ["piston-rs", "piston", "emkc", "code"]
13 | categories = ["api-bindings", "asynchronous"]
14 |
15 | [lib]
16 | name = "piston_rs"
17 |
18 | [dependencies]
19 | serde = { version = "1", features = ["derive"] }
20 |
21 | [dependencies.reqwest]
22 | version = "0.11"
23 | default-features = false
24 | features = ["json", "rustls-tls"]
25 |
26 | [dev-dependencies]
27 | tokio = { version = "1", features = ["macros"] }
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Jonxslays
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
piston_rs
2 |
3 |
4 |
5 |
6 |
7 |
8 | An async wrapper for the Piston code execution engine.
9 |
10 | ## Why piston_rs
11 |
12 | piston_rs aims to make interacting with Piston fun and easy. Your main
13 | tools are the [`Client`](https://docs.rs/piston_rs/latest/piston_rs/struct.Client.html)
14 | and [`Executor`](https://docs.rs/piston_rs/latest/piston_rs/struct.Executor.html) structs.
15 |
16 | The [`Executor`](https://docs.rs/piston_rs/latest/piston_rs/struct.Executor.html)
17 | is constructed containing the source code and other metadata about the code you are
18 | running. This is then sent to Piston via the
19 | [`Client`](https://docs.rs/piston_rs/latest/piston_rs/struct.Client.html).
20 |
21 | piston_rs requires Rust version 1.46.0 or greater.
22 |
23 | ## Getting started
24 |
25 | For more details, check out the [documentation](https://docs.rs/piston_rs/latest)!
26 |
27 | ### Add piston_rs to your project
28 |
29 | ```toml
30 | # Cargo.toml
31 |
32 | [dependencies]
33 | piston_rs = "^0.4"
34 | ```
35 |
36 | ### Make requests to Piston
37 |
38 | ```rs
39 | // main.rs
40 |
41 | #[tokio::main]
42 | async fn main() {
43 | let client = piston_rs::Client::new();
44 | let executor = piston_rs::Executor::new()
45 | .set_language("rust")
46 | .set_version("*")
47 | .add_file(
48 | piston_rs::File::default()
49 | .set_name("main.rs")
50 | .set_content("fn main() { println!(\"42\"); }")
51 | );
52 |
53 | match client.execute(&executor).await {
54 | Ok(response) => {
55 | println!("Language: {}", response.language);
56 | println!("Version: {}", response.version);
57 |
58 | if let Some(c) = response.compile {
59 | println!("Compilation: {}", c.output);
60 | }
61 |
62 | println!("Output: {}", response.run.output);
63 | }
64 | Err(e) => {
65 | println!("Something went wrong contacting Piston.");
66 | println!("{}", e);
67 | }
68 | }
69 | }
70 | ```
71 |
72 | ## License
73 |
74 | piston_rs is licensed under the [MIT License](https://github.com/Jonxslays/piston_rs/blob/master/LICENSE).
75 |
--------------------------------------------------------------------------------
/scripts/coverage.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Make sure grcov is installed
4 | which grcov &> /dev/null
5 | if [ $? != 0 ]; then
6 | echo "grcov is not installed, please install it with \`cargo install grcov\`."
7 | exit 1
8 | fi
9 |
10 | should_continue() {
11 | read -p "Coverage report requires Rust nightly, install now? [y/n]: " VALIDATOR
12 |
13 | case $VALIDATOR in
14 | "y"|"Y"|"yes"|"Yes") echo;;
15 | "n"|"N"|"no"|"No") echo; echo "Rust nightly not installed, exiting..."; exit 1;;
16 | *) echo "Invalid input..."; should_continue;;
17 | esac
18 | }
19 |
20 | ACTIVE_TOOLCHAIN=$(rustup show active-toolchain)
21 | if [ ! $(grep -o "nightly" <<< $ACTIVE_TOOLCHAIN) ]; then
22 | if [ ! $(rustup toolchain list | grep -o "nightly") ]; then
23 | should_continue
24 | fi
25 |
26 | echo "Activating rust nightly..."
27 | rustup default nightly
28 | fi
29 |
30 | if [ ! $(rustup component list | grep -o "llvm-tools") ]; then
31 | echo "Could not find llvm-tools, installing..."
32 | rustup component add llvm-tools-preview
33 | else
34 | echo "Found llvm-tools..."
35 | fi
36 |
37 | echo "Setting up environment..."
38 | export CARGO_INCREMENTAL=0
39 | export RUSTFLAGS="-Cinstrument-coverage"
40 | export RUSTDOCFLAGS="-Cinstrument-coverage -Zunstable-options --persist-doctests target/debug/doctestbins"
41 | export LLVM_PROFILE_FILE="piston_rs-%p-%m.profraw"
42 |
43 | echo "Running tests..."
44 | cargo test
45 |
46 | echo "Generating coverage report..."
47 | grcov . -s . -t html -o ./target/debug/coverage/ \
48 | --binary-path ./target/debug/ \
49 | --ignore-not-existing \
50 | --branch \
51 | --excl-br-line "#\[derive\(" \
52 | --excl-line "#\[derive\("
53 |
54 | echo
55 | echo "Cleaning up..."
56 | rm -f ./*.profraw
57 | rm -rf ./target/debug/doctestbins
58 |
59 | echo
60 | echo "Reverting to previous default toolchain..."
61 | rustup default $(awk '{print $1}' <<< $ACTIVE_TOOLCHAIN)
62 |
63 | echo "Done!"
64 | echo
65 |
66 | COVERAGE=$(grep -oP "message\":\"\K(\d+)" target/debug/coverage/coverage.json)
67 | if [ $COVERAGE -lt 80 ]; then
68 | echo "Coverage failing with: $COVERAGE%";
69 | exit 1;
70 | else
71 | echo "Coverage passing with: $COVERAGE%";
72 | exit 0;
73 | fi
74 |
--------------------------------------------------------------------------------
/src/client.rs:
--------------------------------------------------------------------------------
1 | use std::error::Error;
2 |
3 | use reqwest::header::{HeaderMap, HeaderValue};
4 |
5 | use super::executor::RawExecResponse;
6 | use super::ExecResponse;
7 | use super::ExecResult;
8 | use super::Executor;
9 | use super::Runtime;
10 |
11 | /// A client used to send requests to Piston.
12 | #[derive(Debug, Clone)]
13 | pub struct Client {
14 | /// The base url for Piston.
15 | url: String,
16 | /// The reqwest client to use.
17 | client: reqwest::Client,
18 | /// The headers to send with each request.
19 | headers: HeaderMap,
20 | }
21 |
22 | impl Default for Client {
23 | /// Creates a new client. Alias for [`Client::new`].
24 | ///
25 | /// # Returns
26 | /// - [`Client`] - The new Client.
27 | ///
28 | /// # Example
29 | /// ```
30 | /// let client = piston_rs::Client::default();
31 | ///
32 | /// assert!(client.get_headers().contains_key("Accept"));
33 | /// assert!(client.get_headers().contains_key("User-Agent"));
34 | /// assert!(!client.get_headers().contains_key("Authorization"));
35 | /// assert_eq!(client.get_url(), "https://emkc.org/api/v2/piston".to_string());
36 | /// ```
37 | fn default() -> Self {
38 | Self::new()
39 | }
40 | }
41 |
42 | impl Client {
43 | /// Creates a new client.
44 | ///
45 | /// # Returns
46 | /// - [`Client`] - The new Client.
47 | ///
48 | /// # Example
49 | /// ```
50 | /// let client = piston_rs::Client::new();
51 | ///
52 | /// assert!(client.get_headers().contains_key("Accept"));
53 | /// assert!(client.get_headers().contains_key("User-Agent"));
54 | /// assert!(!client.get_headers().contains_key("Authorization"));
55 | /// ```
56 | pub fn new() -> Self {
57 | Self {
58 | url: "https://emkc.org/api/v2/piston".to_string(),
59 | client: reqwest::Client::new(),
60 | headers: Self::generate_headers(None),
61 | }
62 | }
63 |
64 | /// Creates a new Client with a url that runs the piston code execution engine.
65 | ///
66 | /// This makes it possible to interact with a self-hosted instance of piston.
67 | ///
68 | /// # Arguments
69 | /// - `url` - The url to use as the underlying piston backend.
70 | ///
71 | /// # Returns
72 | /// - [`Client`] - The new Client.
73 | ///
74 | /// # Example
75 | /// ```
76 | /// let client = piston_rs::Client::with_url("http://localhost:3000");
77 | /// assert_eq!(client.get_url(), "http://localhost:3000");
78 | /// ```
79 | pub fn with_url(url: &str) -> Self {
80 | Self {
81 | url: url.to_string(),
82 | client: reqwest::Client::new(),
83 | headers: Self::generate_headers(None),
84 | }
85 | }
86 |
87 | /// Creates a new client, with an api key.
88 | ///
89 | /// # Arguments
90 | /// - `key` - The api key to use.
91 | ///
92 | /// # Returns
93 | /// - [`Client`] - The new Client.
94 | ///
95 | /// # Example
96 | /// ```
97 | /// let client = piston_rs::Client::with_key("123abc");
98 | ///
99 | /// assert!(client.get_headers().contains_key("Authorization"));
100 | /// assert_eq!(client.get_headers().get("Authorization").unwrap(), "123abc");
101 | /// ```
102 | pub fn with_key(key: &str) -> Self {
103 | Self {
104 | url: "https://emkc.org/api/v2/piston".to_string(),
105 | client: reqwest::Client::new(),
106 | headers: Self::generate_headers(Some(key)),
107 | }
108 | }
109 |
110 | /// Creates a new Client using a url and an api key.
111 | ///
112 | /// # Arguments
113 | /// - `url` - The url to use as the underlying piston backend.
114 | /// - `key` - The api key to use.
115 | ///
116 | /// # Returns
117 | /// - [`Client`] - The new Client.
118 | ///
119 | /// # Example
120 | /// ```
121 | /// let client = piston_rs::Client::with_url_and_key("http://localhost:3000", "123abc");
122 | /// assert_eq!(client.get_url(), "http://localhost:3000");
123 | /// assert!(client.get_headers().contains_key("Authorization"));
124 | /// assert_eq!(client.get_headers().get("Authorization").unwrap(), "123abc");
125 | /// ```
126 | pub fn with_url_and_key(url: &str, key: &str) -> Self {
127 | Self {
128 | url: url.to_string(),
129 | client: reqwest::Client::new(),
130 | headers: Self::generate_headers(Some(key)),
131 | }
132 | }
133 |
134 | /// The base url for the Piston V2 API that is being used by this client.
135 | ///
136 | /// # Returns
137 | ///
138 | /// - [`String`] - The requested url.
139 | ///
140 | /// # Example
141 | /// ```
142 | /// let client = piston_rs::Client::new();
143 | ///
144 | /// assert_eq!(client.get_url(), "https://emkc.org/api/v2/piston".to_string());
145 | /// ```
146 | pub fn get_url(&self) -> String {
147 | self.url.clone()
148 | }
149 |
150 | /// The headers being used by this client.
151 | ///
152 | /// # Returns
153 | ///
154 | /// - [`HeaderMap`] - A map of Header key, value pairs.
155 | ///
156 | /// # Example
157 | /// ```
158 | /// let client = piston_rs::Client::new();
159 | /// let headers = client.get_headers();
160 | ///
161 | /// assert_eq!(headers.get("Accept").unwrap(), "application/json");
162 | /// ```
163 | pub fn get_headers(&self) -> HeaderMap {
164 | self.headers.clone()
165 | }
166 |
167 | /// Generates the headers the client should use.
168 | ///
169 | /// # Returns
170 | ///
171 | /// - [`HeaderMap`] - A map of Header key, value pairs.
172 | ///
173 | /// # Example
174 | /// ```ignore # Fails to compile (private function)
175 | /// let headers = piston_rs::Client::generate_headers(None);
176 | ///
177 | /// assert!(!headers.contains_key("Authorization"));
178 | /// assert_eq!(headers.get("Accept").unwrap(), "application/json");
179 | /// assert_eq!(headers.get("User-Agent").unwrap(), "piston-rs");
180 | ///
181 | /// let headers = piston_rs::Client::generate_headers(Some("123abc"));
182 | ///
183 | /// assert_eq!(headers.get("Authorization").unwrap(), "123abc");
184 | /// assert_eq!(headers.get("Accept").unwrap(), "application/json");
185 | /// assert_eq!(headers.get("User-Agent").unwrap(), "piston-rs");
186 | /// ```
187 | fn generate_headers(key: Option<&str>) -> HeaderMap {
188 | let mut headers = HeaderMap::with_capacity(3);
189 | headers.insert("Accept", HeaderValue::from_str("application/json").unwrap());
190 | headers.insert("User-Agent", HeaderValue::from_str("piston-rs").unwrap());
191 |
192 | if let Some(k) = key {
193 | headers.insert("Authorization", HeaderValue::from_str(k).unwrap());
194 | };
195 |
196 | headers
197 | }
198 |
199 | /// Fetches the runtimes from Piston. **This is an http request**.
200 | ///
201 | /// # Returns
202 | /// - [`Result, Box>`] - The available
203 | /// runtimes or the error, if any.
204 | ///
205 | /// # Example
206 | /// ```no_run
207 | /// # #[tokio::test]
208 | /// # async fn test_fetch_runtimes() {
209 | /// let client = piston_rs::Client::new();
210 | ///
211 | /// if let Ok(runtimes) = client.fetch_runtimes().await {
212 | /// assert!(!runtimes.is_empty());
213 | /// } else {
214 | /// // There was an error contacting Piston.
215 | /// }
216 | /// # }
217 | /// ```
218 | pub async fn fetch_runtimes(&self) -> Result, Box> {
219 | let endpoint = format!("{}/runtimes", self.url);
220 | let runtimes = self
221 | .client
222 | .get(endpoint)
223 | .headers(self.headers.clone())
224 | .send()
225 | .await?
226 | .json::>()
227 | .await?;
228 |
229 | Ok(runtimes)
230 | }
231 |
232 | /// Executes code using a given executor. **This is an http
233 | /// request**.
234 | ///
235 | /// # Arguments
236 | /// - `executor` - The executor to use.
237 | ///
238 | /// # Returns
239 | /// - [`Result>`] - The response
240 | /// from Piston or the error, if any.
241 | ///
242 | /// # Example
243 | /// ```no_run
244 | /// # #[tokio::test]
245 | /// # async fn test_execute() {
246 | /// let client = piston_rs::Client::new();
247 | /// let executor = piston_rs::Executor::new()
248 | /// .set_language("rust")
249 | /// .set_version("1.50.0")
250 | /// .add_file(piston_rs::File::default().set_content(
251 | /// "fn main() { println!(\"42\"); }",
252 | /// ));
253 | ///
254 | /// if let Ok(response) = client.execute(&executor).await {
255 | /// assert!(response.compile.is_some());
256 | /// assert!(response.run.is_ok());
257 | /// assert!(response.is_ok());
258 | /// } else {
259 | /// // There was an error contacting Piston.
260 | /// }
261 | /// # }
262 | /// ```
263 | pub async fn execute(&self, executor: &Executor) -> Result> {
264 | let endpoint = format!("{}/execute", self.url);
265 |
266 | match self
267 | .client
268 | .post(endpoint)
269 | .headers(self.headers.clone())
270 | .json::(executor)
271 | .send()
272 | .await
273 | {
274 | Ok(data) => {
275 | let status = data.status();
276 |
277 | match status {
278 | reqwest::StatusCode::OK => {
279 | let response = data.json::().await?;
280 |
281 | Ok(ExecResponse {
282 | language: response.language,
283 | version: response.version,
284 | run: response.run,
285 | compile: response.compile,
286 | status: status.as_u16(),
287 | })
288 | }
289 | _ => {
290 | let text = format!("{}: {}", data.status(), data.text().await?);
291 |
292 | let exec_result = ExecResult {
293 | stdout: String::new(),
294 | stderr: text.clone(),
295 | output: text,
296 | code: Some(1),
297 | signal: None,
298 | };
299 |
300 | let exec_response = ExecResponse {
301 | language: executor.language.clone(),
302 | version: executor.version.clone(),
303 | run: exec_result,
304 | compile: None,
305 | status: status.as_u16(),
306 | };
307 |
308 | Ok(exec_response)
309 | }
310 | }
311 | }
312 | Err(e) => Err(Box::new(e)),
313 | }
314 | }
315 | }
316 |
317 | #[cfg(test)]
318 | mod test_client_private {
319 | use super::Client;
320 |
321 | #[test]
322 | fn test_gen_headers_no_key() {
323 | let headers = Client::generate_headers(None);
324 |
325 | assert!(!headers.contains_key("Authorization"));
326 | assert_eq!(headers.get("Accept").unwrap(), "application/json");
327 | assert_eq!(headers.get("User-Agent").unwrap(), "piston-rs");
328 | }
329 |
330 | #[test]
331 | fn test_gen_headers_with_key() {
332 | let headers = Client::generate_headers(Some("123abc"));
333 |
334 | assert_eq!(headers.get("Authorization").unwrap(), "123abc");
335 | assert_eq!(headers.get("Accept").unwrap(), "application/json");
336 | assert_eq!(headers.get("User-Agent").unwrap(), "piston-rs");
337 | }
338 | }
339 |
--------------------------------------------------------------------------------
/src/executor.rs:
--------------------------------------------------------------------------------
1 | use serde::{Deserialize, Serialize};
2 |
3 | use super::File;
4 |
5 | /// The result of code execution returned by Piston.
6 | #[derive(Clone, Debug, Serialize, Deserialize)]
7 | pub struct ExecResult {
8 | /// The text sent to `stdout` during execution.
9 | pub stdout: String,
10 | /// The text sent to `stderr` during execution.
11 | pub stderr: String,
12 | /// The text sent to both `stdout`, and `stderr` during execution.
13 | pub output: String,
14 | /// The optional exit code returned by the process.
15 | pub code: Option,
16 | /// The optional signal sent to the process. (`SIGKILL` etc)
17 | pub signal: Option,
18 | }
19 |
20 | impl ExecResult {
21 | /// Whether or not the execution was ok.
22 | ///
23 | /// # Returns
24 | /// - [`bool`] - [`true`] if the execution returned a zero exit
25 | /// code.
26 | pub fn is_ok(&self) -> bool {
27 | self.code.is_some() && self.code.unwrap() == 0
28 | }
29 |
30 | /// Whether or not the execution produced errors.
31 | ///
32 | /// # Returns
33 | /// - [`bool`] - [`true`] if the execution returned a non zero exit
34 | /// code.
35 | pub fn is_err(&self) -> bool {
36 | self.code.is_some() && self.code.unwrap() != 0
37 | }
38 | }
39 |
40 | /// Raw response received from Piston
41 | #[doc(hidden)]
42 | #[derive(Clone, Debug, Serialize, Deserialize)]
43 | pub struct RawExecResponse {
44 | /// The language that was used.
45 | pub language: String,
46 | /// The version of the language that was used.
47 | pub version: String,
48 | /// The result Piston sends detailing execution.
49 | pub run: ExecResult,
50 | /// The optional result Piston sends detailing compilation. This
51 | /// will be [`None`] for non-compiled languages.
52 | pub compile: Option,
53 | }
54 |
55 | /// A response returned by Piston when executing code.
56 | #[derive(Clone, Debug, Serialize, Deserialize)]
57 | pub struct ExecResponse {
58 | /// The language that was used.
59 | pub language: String,
60 | /// The version of the language that was used.
61 | pub version: String,
62 | /// The result Piston sends detailing execution.
63 | pub run: ExecResult,
64 | /// The optional result Piston sends detailing compilation. This
65 | /// will be [`None`] for non-compiled languages.
66 | pub compile: Option,
67 | /// The response status returned by Piston.
68 | pub status: u16,
69 | }
70 |
71 | impl ExecResponse {
72 | /// Whether or not the request to Piston succeeded.
73 | ///
74 | /// # Returns
75 | /// - [`bool`] - [`true`] if a 200 status code was received from Piston.
76 | pub fn is_ok(&self) -> bool {
77 | self.status == 200
78 | }
79 |
80 | /// Whether or not the request to Piston failed.
81 | ///
82 | /// # Returns
83 | /// - [`bool`] - [`true`] if a non 200 status code was received from Piston.
84 | pub fn is_err(&self) -> bool {
85 | self.status != 200
86 | }
87 | }
88 |
89 | /// An object containing information about the code being executed.
90 | ///
91 | /// A convenient builder flow is provided by the methods associated with
92 | /// the `Executor`. These consume self and return self for chained calls.
93 | #[derive(Clone, Debug, Serialize, Deserialize)]
94 | pub struct Executor {
95 | /// **Required** - The language to use for execution. Defaults to a
96 | /// new `String`.
97 | pub language: String,
98 | /// The version of the language to use for execution.
99 | /// Defaults to "*" (*most recent version*).
100 | pub version: String,
101 | /// **Required** - A `Vector` of `File`'s to send to Piston. The
102 | /// first file in the vector is considered the main file. Defaults
103 | /// to a new `Vector`.
104 | pub files: Vec,
105 | /// The text to pass as stdin to the program. Defaults to a new
106 | /// `String`.
107 | pub stdin: String,
108 | /// The arguments to pass to the program. Defaults to a new
109 | /// `Vector`.
110 | pub args: Vec,
111 | /// The maximum allowed time for compilation in milliseconds.
112 | /// Defaults to `10,000`.
113 | pub compile_timeout: isize,
114 | /// The maximum allowed time for execution in milliseconds. Defaults
115 | /// to `3,000`.
116 | pub run_timeout: isize,
117 | /// The maximum allowed memory usage for compilation in bytes.
118 | /// Defaults to `-1` (*no limit*).
119 | pub compile_memory_limit: isize,
120 | /// The maximum allowed memory usage for execution in bytes.
121 | /// Defaults to `-1` (*no limit*).
122 | pub run_memory_limit: isize,
123 | }
124 |
125 | impl Default for Executor {
126 | /// Creates a new executor. Alias for [`Executor::new`].
127 | ///
128 | /// # Returns
129 | /// - [`Executor`] - The new blank Executor.
130 | ///
131 | /// # Example
132 | /// ```
133 | /// let executor = piston_rs::Executor::default();
134 | ///
135 | /// assert_eq!(executor.language, String::new());
136 | /// assert_eq!(executor.version, String::from("*"));
137 | /// ```
138 | fn default() -> Self {
139 | Self::new()
140 | }
141 | }
142 |
143 | impl Executor {
144 | /// Creates a new executor representing source code to be
145 | /// executed.
146 | ///
147 | /// Metadata regarding the source language and files will
148 | /// need to be added using the associated method calls, and other
149 | /// optional fields can be set as well.
150 | ///
151 | /// # Returns
152 | /// - [`Executor`] - The new blank Executor.
153 | ///
154 | /// # Example
155 | /// ```
156 | /// let executor = piston_rs::Executor::new();
157 | ///
158 | /// assert_eq!(executor.language, String::new());
159 | /// assert_eq!(executor.version, String::from("*"));
160 | /// ```
161 | pub fn new() -> Self {
162 | Self {
163 | language: String::new(),
164 | version: String::from("*"),
165 | files: vec![],
166 | stdin: String::new(),
167 | args: vec![],
168 | compile_timeout: 10000,
169 | run_timeout: 3000,
170 | compile_memory_limit: -1,
171 | run_memory_limit: -1,
172 | }
173 | }
174 |
175 | /// Resets the executor back to a `new` state, ready to be
176 | /// configured again and sent to Piston after metadata is added.
177 | /// This method mutates the existing executor in place.
178 | ///
179 | /// # Example
180 | /// ```
181 | /// let mut executor = piston_rs::Executor::new()
182 | /// .set_language("rust");
183 | ///
184 | /// assert_eq!(executor.language, "rust".to_string());
185 | ///
186 | /// executor.reset();
187 | ///
188 | /// assert_eq!(executor.language, String::new());
189 | /// ```
190 | pub fn reset(&mut self) {
191 | self.language = String::new();
192 | self.version = String::from("*");
193 | self.files = vec![];
194 | self.stdin = String::new();
195 | self.args = vec![];
196 | self.compile_timeout = 10000;
197 | self.run_timeout = 3000;
198 | self.compile_memory_limit = -1;
199 | self.run_memory_limit = -1;
200 | }
201 |
202 | /// Sets the language to use for execution.
203 | ///
204 | /// # Arguments
205 | /// - `language` - The language to use.
206 | ///
207 | /// # Returns
208 | /// - [`Self`] - For chained method calls.
209 | ///
210 | /// # Example
211 | /// ```
212 | /// let executor = piston_rs::Executor::new()
213 | /// .set_language("rust");
214 | ///
215 | /// assert_eq!(executor.language, "rust".to_string());
216 | /// ```
217 | #[must_use]
218 | pub fn set_language(mut self, language: &str) -> Self {
219 | self.language = language.to_lowercase();
220 | self
221 | }
222 |
223 | /// Sets the version of the language to use for execution.
224 | ///
225 | /// # Arguments
226 | /// - `version` - The version to use.
227 | ///
228 | /// # Returns
229 | /// - [`Self`] - For chained method calls.
230 | ///
231 | /// # Example
232 | /// ```
233 | /// let executor = piston_rs::Executor::new()
234 | /// .set_version("1.50.0");
235 | ///
236 | /// assert_eq!(executor.version, "1.50.0".to_string());
237 | /// ```
238 | #[must_use]
239 | pub fn set_version(mut self, version: &str) -> Self {
240 | self.version = version.to_string();
241 | self
242 | }
243 |
244 | /// Adds a [`File`] containing the code to be executed. Does not
245 | /// overwrite any existing files.
246 | ///
247 | /// # Arguments
248 | /// - `file` - The file to add.
249 | ///
250 | /// # Returns
251 | /// - [`Self`] - For chained method calls.
252 | ///
253 | /// # Example
254 | /// ```
255 | /// let file = piston_rs::File::default();
256 | ///
257 | /// let executor = piston_rs::Executor::new()
258 | /// .add_file(file.clone());
259 | ///
260 | /// assert_eq!(executor.files, [file].to_vec());
261 | /// ```
262 | #[must_use]
263 | pub fn add_file(mut self, file: File) -> Self {
264 | self.files.push(file);
265 | self
266 | }
267 |
268 | /// Adds multiple [`File`]'s containing the code to be executed.
269 | /// Does not overwrite any existing files.
270 | ///
271 | /// # Arguments
272 | /// - `files` - The files to add.
273 | ///
274 | /// # Returns
275 | /// - [`Self`] - For chained method calls.
276 | ///
277 | /// # Example
278 | /// ```
279 | /// let mut files = vec![];
280 | ///
281 | /// for _ in 0..3 {
282 | /// files.push(piston_rs::File::default());
283 | /// }
284 | ///
285 | /// let executor = piston_rs::Executor::new()
286 | /// .add_files(files.clone());
287 | ///
288 | /// assert_eq!(executor.files, files);
289 | /// ```
290 | #[must_use]
291 | pub fn add_files(mut self, files: Vec) -> Self {
292 | self.files.extend(files);
293 | self
294 | }
295 |
296 | /// Adds multiple [`File`]'s containing the code to be executed.
297 | /// Overwrites any existing files. This method mutates the existing
298 | /// executor in place. **Overwrites any existing files.**
299 | ///
300 | /// # Arguments
301 | /// - `files` - The files to replace existing files with.
302 | ///
303 | /// # Example
304 | /// ```
305 | /// let old_file = piston_rs::File::default()
306 | /// .set_name("old_file.rs");
307 | ///
308 | /// let mut executor = piston_rs::Executor::new()
309 | /// .add_file(old_file.clone());
310 | ///
311 | /// assert_eq!(executor.files.len(), 1);
312 | /// assert_eq!(executor.files[0].name, "old_file.rs".to_string());
313 | ///
314 | /// let new_files = vec![
315 | /// piston_rs::File::default().set_name("new_file1.rs"),
316 | /// piston_rs::File::default().set_name("new_file2.rs"),
317 | /// ];
318 | ///
319 | /// executor.set_files(new_files.clone());
320 | ///
321 | /// assert_eq!(executor.files.len(), 2);
322 | /// assert_eq!(executor.files[0].name, "new_file1.rs".to_string());
323 | /// assert_eq!(executor.files[1].name, "new_file2.rs".to_string());
324 | /// ```
325 | pub fn set_files(&mut self, files: Vec) {
326 | self.files = files;
327 | }
328 |
329 | /// Sets the text to pass as `stdin` to the program.
330 | ///
331 | /// # Arguments
332 | /// - `stdin` - The text to set.
333 | ///
334 | /// # Returns
335 | /// - [`Self`] - For chained method calls.
336 | ///
337 | /// # Example
338 | /// ```
339 | /// let executor = piston_rs::Executor::new()
340 | /// .set_stdin("Fearless concurrency");
341 | ///
342 | /// assert_eq!(executor.stdin, "Fearless concurrency".to_string());
343 | /// ```
344 | #[must_use]
345 | pub fn set_stdin(mut self, stdin: &str) -> Self {
346 | self.stdin = stdin.to_string();
347 | self
348 | }
349 |
350 | /// Adds an arg to be passed as a command line argument. Does not
351 | /// overwrite any existing args.
352 | ///
353 | /// # Arguments
354 | /// - `arg` - The arg to add.
355 | ///
356 | /// # Returns
357 | /// - [`Self`] - For chained method calls.
358 | ///
359 | /// # Example
360 | /// ```
361 | /// let executor = piston_rs::Executor::new()
362 | /// .add_arg("--verbose");
363 | ///
364 | /// assert_eq!(executor.args, vec!["--verbose".to_string()]);
365 | /// ```
366 | #[must_use]
367 | pub fn add_arg(mut self, arg: &str) -> Self {
368 | self.args.push(arg.to_string());
369 | self
370 | }
371 |
372 | /// Adds multiple args to be passed as a command line arguments.
373 | /// Does not overwrite any existing args.
374 | ///
375 | /// # Arguments
376 | /// - `args` - The args to add.
377 | ///
378 | /// # Example
379 | /// ```
380 | /// let executor = piston_rs::Executor::new()
381 | /// .add_args(vec!["--verbose"]);
382 | ///
383 | /// assert_eq!(executor.args, vec!["--verbose".to_string()]);
384 | /// ```
385 | #[must_use]
386 | pub fn add_args(mut self, args: Vec<&str>) -> Self {
387 | self.args.extend(args.iter().map(|a| a.to_string()));
388 | self
389 | }
390 |
391 | /// Adds multiple args to be passed as a command line arguments.
392 | /// Overwrites any existing args. This method mutates the existing
393 | /// executor in place. **Overwrites any existing args.**
394 | ///
395 | /// # Arguments
396 | /// - `args` - The args to replace existing args with.
397 | ///
398 | /// # Example
399 | /// ```
400 | /// let mut executor = piston_rs::Executor::new()
401 | /// .add_arg("--verbose");
402 | ///
403 | /// assert_eq!(executor.args.len(), 1);
404 | /// assert_eq!(executor.args[0], "--verbose".to_string());
405 | ///
406 | /// let args = vec!["commit", "-S"];
407 | /// executor.set_args(args);
408 | ///
409 | /// assert_eq!(executor.args.len(), 2);
410 | /// assert_eq!(executor.args[0], "commit".to_string());
411 | /// assert_eq!(executor.args[1], "-S".to_string());
412 | /// ```
413 | pub fn set_args(&mut self, args: Vec<&str>) {
414 | self.args = args.iter().map(|a| a.to_string()).collect();
415 | }
416 |
417 | /// Sets the maximum allowed time for compilation in milliseconds.
418 | ///
419 | /// # Arguments
420 | /// - `timeout` - The timeout to set.
421 | ///
422 | /// # Returns
423 | /// - [`Self`] - For chained method calls.
424 | ///
425 | /// # Example
426 | /// ```
427 | /// let executor = piston_rs::Executor::new()
428 | /// .set_compile_timeout(5000);
429 | ///
430 | /// assert_eq!(executor.compile_timeout, 5000);
431 | /// ```
432 | #[must_use]
433 | pub fn set_compile_timeout(mut self, timeout: isize) -> Self {
434 | self.compile_timeout = timeout;
435 | self
436 | }
437 |
438 | /// Sets the maximum allowed time for execution in milliseconds.
439 | ///
440 | /// # Arguments
441 | /// - `timeout` - The timeout to set.
442 | ///
443 | /// # Returns
444 | /// - [`Self`] - For chained method calls.
445 | ///
446 | /// # Example
447 | /// ```
448 | /// let executor = piston_rs::Executor::new()
449 | /// .set_run_timeout(1500);
450 | ///
451 | /// assert_eq!(executor.run_timeout, 1500);
452 | /// ```
453 | #[must_use]
454 | pub fn set_run_timeout(mut self, timeout: isize) -> Self {
455 | self.run_timeout = timeout;
456 | self
457 | }
458 |
459 | /// Sets the maximum allowed memory usage for compilation in bytes.
460 | ///
461 | /// # Arguments
462 | /// - `limit` - The memory limit to set.
463 | ///
464 | /// # Returns
465 | /// - [`Self`] - For chained method calls.
466 | ///
467 | /// # Example
468 | /// ```
469 | /// let executor = piston_rs::Executor::new()
470 | /// .set_compile_memory_limit(100_000_000);
471 | ///
472 | /// assert_eq!(executor.compile_memory_limit, 100_000_000);
473 | /// ```
474 | #[must_use]
475 | pub fn set_compile_memory_limit(mut self, limit: isize) -> Self {
476 | self.compile_memory_limit = limit;
477 | self
478 | }
479 |
480 | /// Sets the maximum allowed memory usage for execution in bytes.
481 | ///
482 | /// # Arguments
483 | /// - `limit` - The memory limit to set.
484 | ///
485 | /// # Returns
486 | /// - [`Self`] - For chained method calls.
487 | ///
488 | /// # Example
489 | /// ```
490 | /// let executor = piston_rs::Executor::new()
491 | /// .set_run_memory_limit(100_000_000);
492 | ///
493 | /// assert_eq!(executor.run_memory_limit, 100_000_000);
494 | /// ```
495 | #[must_use]
496 | pub fn set_run_memory_limit(mut self, limit: isize) -> Self {
497 | self.run_memory_limit = limit;
498 | self
499 | }
500 | }
501 |
502 | #[cfg(test)]
503 | mod test_execution_result {
504 | use super::ExecResponse;
505 | use super::ExecResult;
506 |
507 | /// Generates an ExecResult for testing
508 | fn generate_result(stdout: &str, stderr: &str, code: isize) -> ExecResult {
509 | ExecResult {
510 | stdout: stdout.to_string(),
511 | stderr: stderr.to_string(),
512 | output: format!("{}\n{}", stdout, stderr),
513 | code: Some(code),
514 | signal: None,
515 | }
516 | }
517 |
518 | /// Generates an ExecResponse for testing.
519 | fn generate_response(status: u16) -> ExecResponse {
520 | ExecResponse {
521 | language: "rust".to_string(),
522 | version: "1.50.0".to_string(),
523 | run: generate_result("Be unique.", "", 0),
524 | compile: None,
525 | status,
526 | }
527 | }
528 |
529 | #[test]
530 | fn test_response_is_ok() {
531 | let response = generate_response(200);
532 |
533 | assert!(response.is_ok());
534 | assert!(!response.is_err());
535 | }
536 |
537 | #[test]
538 | fn test_response_is_err() {
539 | let response = generate_response(400);
540 |
541 | assert!(!response.is_ok());
542 | assert!(response.is_err());
543 | }
544 |
545 | #[test]
546 | fn test_result_is_ok() {
547 | let result = generate_result("Hello, world", "", 0);
548 |
549 | assert!(result.is_ok());
550 | assert!(!result.is_err());
551 | }
552 |
553 | #[test]
554 | fn test_result_is_err() {
555 | let result = generate_result("", "Error!", 1);
556 |
557 | assert!(!result.is_ok());
558 | assert!(result.is_err());
559 | }
560 |
561 | #[test]
562 | fn test_is_err_with_stdout() {
563 | let result = generate_result("Hello, world", "Error!", 1);
564 |
565 | assert!(!result.is_ok());
566 | assert!(result.is_err());
567 | }
568 | }
569 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! `piston_rs` - An async wrapper for the
2 | //! [Piston](https://github.com/engineer-man/piston) code execution
3 | //! engine.
4 | //!
5 | //! Aiming to make interacting with Piston fun and easy.
6 | //!
7 | //! ## Getting started
8 | //!
9 | //! Check out the [`Client`] and [`Executor`] documentation.
10 | //!
11 | //! ##### Make requests to Piston
12 | //!
13 | //! ```
14 | //! # #[tokio::test]
15 | //! # async fn example() {
16 | //! let client = piston_rs::Client::new();
17 | //! let executor = piston_rs::Executor::new()
18 | //! .set_language("rust")
19 | //! .set_version("*")
20 | //! .add_file(
21 | //! piston_rs::File::default()
22 | //! .set_name("main.rs")
23 | //! .set_content("fn main() { println!(\"42\"); }")
24 | //! );
25 | //!
26 | //! match client.execute(&executor).await {
27 | //! Ok(response) => {
28 | //! println!("Language: {}", response.language);
29 | //! println!("Version: {}", response.version);
30 | //!
31 | //! if let Some(c) = response.compile {
32 | //! println!("Compilation: {}", c.output);
33 | //! }
34 | //!
35 | //! println!("Output: {}", response.run.output);
36 | //! }
37 | //! Err(e) => {
38 | //! println!("Something went wrong contacting Piston.");
39 | //! println!("{}", e);
40 | //! }
41 | //! }
42 | //! # }
43 | //! ```
44 |
45 | // RIP shrimpie, gone but not forgotten.
46 |
47 | use serde::{Deserialize, Serialize};
48 | use std::fs;
49 | use std::path::{Path, PathBuf};
50 |
51 | mod client;
52 | mod executor;
53 |
54 | pub use client::Client;
55 | pub use executor::ExecResponse;
56 | pub use executor::ExecResult;
57 | pub use executor::Executor;
58 |
59 | /// A runtime available to be used by Piston.
60 | ///
61 | /// ##### Note
62 | ///
63 | /// Runtimes are not meant to be created manually. Instead, they should
64 | /// be fetched from Piston using [`Client::fetch_runtimes`] and stored,
65 | /// if you have a need for the information.
66 | #[derive(Clone, Debug, Serialize, Deserialize)]
67 | pub struct Runtime {
68 | /// The language.
69 | pub language: String,
70 | /// The version of the language.
71 | pub version: String,
72 | /// The aliases associated with this runtime.
73 | pub aliases: Vec,
74 | }
75 |
76 | /// The result from attempting to load a [`File`].
77 | type LoadResult = Result;
78 |
79 | /// The error that is returned when loading from a [`File`] on disk
80 | /// fails for any reason.
81 | #[derive(Debug, Clone)]
82 | pub struct LoadError {
83 | /// The details of this error.
84 | pub details: String,
85 | }
86 |
87 | impl LoadError {
88 | /// Generates a new [`LoadError`].
89 | ///
90 | /// # Arguments
91 | /// - `details` - The details of the error.
92 | ///
93 | /// # Returns
94 | /// - [`LoadError`] - The new error.
95 | ///
96 | /// # Examples
97 | /// ```
98 | /// let e = piston_rs::LoadError::new("err");
99 | ///
100 | /// assert_eq!(e.details, "err".to_string())
101 | /// ```
102 | pub fn new(details: &str) -> Self {
103 | Self {
104 | details: details.into(),
105 | }
106 | }
107 | }
108 |
109 | impl std::fmt::Display for LoadError {
110 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 | write!(f, "{}", self.details)
112 | }
113 | }
114 |
115 | /// A file that contains source code to be executed.
116 | #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
117 | pub struct File {
118 | // The name of the file. Defaults to a new `String`.
119 | pub name: String,
120 | /// **Required** The content of the file.
121 | pub content: String,
122 | /// The encoding of the file. Defaults to "utf8".
123 | pub encoding: String,
124 | }
125 |
126 | impl Default for File {
127 | /// Creates an unnamed new [`File`] with utf8 encoding and no
128 | /// content.
129 | ///
130 | /// # Returns
131 | /// - [`File`] - The new blank File.
132 | ///
133 | /// # Example
134 | /// ```
135 | /// let file = piston_rs::File::default();
136 | ///
137 | /// assert_eq!(file.name, String::new());
138 | /// assert_eq!(file.content, String::new());
139 | /// assert_eq!(file.encoding, "utf8".to_string());
140 | /// ```
141 | fn default() -> Self {
142 | Self {
143 | name: String::new(),
144 | content: String::new(),
145 | encoding: String::from("utf8"),
146 | }
147 | }
148 | }
149 |
150 | impl File {
151 | /// Creates a new [`File`].
152 | ///
153 | /// # Arguments
154 | /// - `name` - The name to use.
155 | /// - `content` - The content to use.
156 | /// - `encoding` - The encoding to use. Must be one of "utf8",
157 | /// "hex", or "base64".
158 | ///
159 | /// # Returns
160 | /// - [`File`] - The new File.
161 | ///
162 | /// # Example
163 | /// ```
164 | /// let file = piston_rs::File::new(
165 | /// "script.sh",
166 | /// "ZWNobyBIZWxsbywgV29ybGQh",
167 | /// "base64",
168 | /// );
169 | ///
170 | /// assert!(file.content.contains("ZWNobyBIZWxsbywgV29ybGQh"));
171 | /// assert_eq!(file.name, "script.sh".to_string());
172 | /// assert_eq!(file.encoding, "base64".to_string());
173 | /// ```
174 | pub fn new(name: &str, content: &str, encoding: &str) -> Self {
175 | Self {
176 | name: name.to_string(),
177 | content: content.to_string(),
178 | encoding: encoding.to_string(),
179 | }
180 | }
181 |
182 | /// Creates a new [`File`] from an existing file on disk.
183 | ///
184 | /// # Arguments
185 | /// - `path` - The path to the file.
186 | ///
187 | /// # Returns
188 | /// - [`File`] - The new File.
189 | ///
190 | /// # Example
191 | /// ```
192 | /// let file = piston_rs::File::load_from("src/lib.rs").unwrap();
193 | ///
194 | /// assert!(file.content.contains("pub fn load_from"));
195 | /// assert_eq!(file.name, "lib.rs".to_string());
196 | /// assert_eq!(file.encoding, "utf8".to_string());
197 | /// ```
198 | pub fn load_from(path: &str) -> LoadResult {
199 | let path = PathBuf::from(path);
200 |
201 | if !path.is_file() {
202 | return Err(LoadError::new("File does not exist, or is a directory"));
203 | }
204 |
205 | let name = match path.file_name() {
206 | Some(n) => n.to_string_lossy(),
207 | None => {
208 | return Err(LoadError::new("Unable to parse file name"));
209 | }
210 | };
211 |
212 | Ok(Self {
213 | name: name.to_string(),
214 | content: File::load_contents(&path)?,
215 | encoding: String::from("utf8"),
216 | })
217 | }
218 |
219 | /// Loads the contents of the given file.
220 | ///
221 | /// # Arguments
222 | /// - `path` - The path to the file.
223 | ///
224 | /// # Returns
225 | /// - [`String`] - The file's contents.
226 | ///
227 | /// # Example
228 | /// ```ignore # Fails to compile (private function)
229 | /// let content = piston_rs::File::load_contents("src/lib.rs").unwrap();
230 | ///
231 | /// assert!(content.contains("fn load_contents"));
232 | /// ```
233 | fn load_contents(path: &Path) -> LoadResult {
234 | match fs::read_to_string(path) {
235 | Ok(content) => Ok(content),
236 | Err(e) => Err(LoadError::new(&e.to_string())),
237 | }
238 | }
239 |
240 | /// Sets the content of the file.
241 | ///
242 | /// # Arguments
243 | /// - `content` - The content to use.
244 | ///
245 | /// # Returns
246 | /// - [`Self`] - For chained method calls.
247 | ///
248 | /// # Example
249 | /// ```
250 | /// let file = piston_rs::File::default()
251 | /// .set_content("print(\"Hello, world!\")");
252 | ///
253 | /// assert_eq!(file.content, "print(\"Hello, world!\")".to_string());
254 | /// ```
255 | #[must_use]
256 | pub fn set_content(mut self, content: &str) -> Self {
257 | self.content = content.to_string();
258 | self
259 | }
260 |
261 | /// Sets the content of the file to the contents of an existing
262 | /// file on disk.
263 | ///
264 | /// # Arguments
265 | /// - `path` - The path to the file.
266 | ///
267 | /// # Returns
268 | /// - [`Self`] - For chained method calls.
269 | ///
270 | /// # Example
271 | /// ```
272 | /// let file = piston_rs::File::default()
273 | /// .load_content_from("src/lib.rs");
274 | ///
275 | /// assert!(file.is_ok());
276 | /// assert!(file.unwrap().content.contains("pub fn load_content_from"));
277 | /// ```
278 | pub fn load_content_from(mut self, path: &str) -> LoadResult {
279 | let path = PathBuf::from(path);
280 | self.content = File::load_contents(&path)?;
281 | Ok(self)
282 | }
283 |
284 | /// Sets the name of the file.
285 | ///
286 | /// # Arguments
287 | /// - `name` - The name to use.
288 | ///
289 | /// # Returns
290 | /// - [`Self`] - For chained method calls.
291 | ///
292 | /// # Example
293 | /// ```
294 | /// let file = piston_rs::File::default()
295 | /// .set_name("__main__.py");
296 | ///
297 | /// assert_eq!(file.name, "__main__.py".to_string());
298 | /// ```
299 | #[must_use]
300 | pub fn set_name(mut self, name: &str) -> Self {
301 | self.name = name.to_string();
302 | self
303 | }
304 |
305 | /// Sets the encoding of the file.
306 | ///
307 | /// # Arguments
308 | /// - `encoding` - The encoding to use. Must be one of "utf8",
309 | /// "hex", or "base64".
310 | ///
311 | /// # Returns
312 | /// - [`Self`] - For chained method calls.
313 | ///
314 | /// # Example
315 | /// ```
316 | /// let file = piston_rs::File::default()
317 | /// .set_encoding("hex");
318 | ///
319 | /// assert_eq!(file.encoding, "hex".to_string());
320 | /// ```
321 | #[must_use]
322 | pub fn set_encoding(mut self, encoding: &str) -> Self {
323 | self.encoding = encoding.to_string();
324 | self
325 | }
326 | }
327 |
328 | #[cfg(test)]
329 | mod test_file_private {
330 | use super::File;
331 | use super::Runtime;
332 | use std::path::PathBuf;
333 |
334 | #[test]
335 | fn test_load_contents() {
336 | let path = PathBuf::from(file!());
337 | let contents = File::load_contents(&path).unwrap();
338 |
339 | assert!(contents.contains("mod test_file_private {"));
340 | }
341 |
342 | #[test]
343 | fn test_load_contents_non_existent() {
344 | let path = PathBuf::from("/path/doesnt/exist");
345 | let contents = File::load_contents(&path);
346 |
347 | assert!(contents.is_err());
348 | let err = contents.unwrap_err();
349 | let expd_err = match cfg!(windows) {
350 | true => String::from("The system cannot find the path specified. (os error 3)"),
351 | false => String::from("No such file or directory (os error 2)"),
352 | };
353 |
354 | assert_eq!(err.details, expd_err);
355 | assert_eq!(format!("{}", err), expd_err);
356 |
357 | let err2 = err.clone();
358 | assert_eq!(err.details, err2.details);
359 | }
360 |
361 | #[test]
362 | fn test_runtime_creation() {
363 | let rt = Runtime {
364 | language: "clojure".to_string(),
365 | version: "9000".to_string(),
366 | aliases: vec![],
367 | };
368 |
369 | let rt2 = rt.clone();
370 | assert_eq!(rt.language, rt2.language);
371 | assert_eq!(rt.version, rt2.version);
372 | assert_eq!(rt.aliases, rt2.aliases);
373 |
374 | assert_eq!(rt.language, "clojure".to_string());
375 | assert_eq!(rt.version, "9000".to_string());
376 | assert!(rt.aliases.is_empty());
377 | }
378 | }
379 |
--------------------------------------------------------------------------------