├── .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 | Crate 4 | Docs 5 | Build 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 | --------------------------------------------------------------------------------