├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── mean_bean_ci.yml │ └── mean_bean_deploy.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── assets ├── leetup.gif ├── progress1.png ├── progress2.png └── progress3.png ├── cache ├── Cargo.toml ├── README.md └── src │ ├── kvstore.rs │ └── lib.rs ├── ci ├── build.bash ├── common.bash ├── set_rust_version.bash └── test.bash ├── docs └── usage.md ├── src ├── client.rs ├── cmd.rs ├── config.rs ├── error.rs ├── icon.rs ├── lib.rs ├── main.rs ├── model │ └── mod.rs ├── printer │ ├── mod.rs │ ├── printer.rs │ ├── submit_execution_printer.rs │ └── test_execution_printer.rs ├── service │ ├── auth.rs │ ├── file.rs │ ├── lang.rs │ ├── leetcode.rs │ ├── mod.rs │ ├── pool.rs │ ├── provider.rs │ └── session.rs └── template.rs ├── tag.sh └── tests └── cli.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | # TODO: Replace with your github or remove. 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/mean_bean_ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build_and_test: 7 | name: Rust project 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Run cargo test 12 | uses: actions-rs/cargo@v1 13 | with: 14 | command: test 15 | 16 | 17 | -------------------------------------------------------------------------------- /.github/workflows/mean_bean_deploy.yml: -------------------------------------------------------------------------------- 1 | name: Mean Bean Deploy 2 | 3 | on: 4 | push: 5 | # Sequence of patterns matched against refs/tags 6 | tags: 7 | - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10 8 | 9 | env: 10 | BIN: leetup 11 | 12 | jobs: 13 | # This job downloads and stores `cross` as an artifact, so that it can be 14 | # redownloaded across all of the jobs. Currently this copied pasted between 15 | # `mean_bean_ci.yml` and `mean_bean_deploy.yml`. Make sure to update both places when making 16 | # changes. 17 | install-cross: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v1 21 | with: 22 | depth: 50 23 | - uses: XAMPPRocky/get-github-release@v1 24 | id: cross 25 | with: 26 | owner: rust-embedded 27 | repo: cross 28 | matches: ${{ matrix.platform }} 29 | token: ${{ secrets.GITHUB_TOKEN }} 30 | - uses: actions/upload-artifact@v1 31 | with: 32 | name: cross-${{ matrix.platform }} 33 | path: ${{ steps.cross.outputs.install_path }} 34 | strategy: 35 | matrix: 36 | platform: [linux-musl, apple-darwin] 37 | 38 | windows: 39 | runs-on: windows-latest 40 | needs: install-cross 41 | strategy: 42 | matrix: 43 | target: 44 | # MSVC 45 | - x86_64-pc-windows-msvc 46 | 47 | steps: 48 | - uses: actions/checkout@v2 49 | - run: bash ci/set_rust_version.bash stable ${{ matrix.target }} 50 | - run: bash ci/build.bash cargo ${{ matrix.target }} RELEASE 51 | - run: | 52 | cd ./target/${{ matrix.target }}/release/ 53 | 7z a "${{ env.BIN }}.zip" "${{ env.BIN }}.exe" 54 | mv "${{ env.BIN }}.zip" $GITHUB_WORKSPACE 55 | shell: bash 56 | # We're using using a fork of `actions/create-release` that detects 57 | # whether a release is already available or not first. 58 | - uses: XAMPPRocky/create-release@v1.0.2 59 | id: create_release 60 | env: 61 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 62 | with: 63 | tag_name: ${{ github.ref }} 64 | release_name: ${{ github.ref }} 65 | # Draft should **always** be false. GitHub doesn't provide a way to 66 | # get draft releases from its API, so there's no point using it. 67 | draft: false 68 | prerelease: false 69 | - uses: actions/upload-release-asset@v1 70 | id: upload-release-asset 71 | env: 72 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 73 | with: 74 | upload_url: ${{ steps.create_release.outputs.upload_url }} 75 | asset_path: ${{ env.BIN }}.zip 76 | asset_name: ${{ env.BIN }}-${{ matrix.target }}.zip 77 | asset_content_type: application/zip 78 | 79 | macos: 80 | runs-on: macos-latest 81 | needs: install-cross 82 | strategy: 83 | matrix: 84 | target: 85 | # macOS 86 | - x86_64-apple-darwin 87 | - aarch64-apple-darwin 88 | steps: 89 | - uses: actions/checkout@v2 90 | - uses: actions/download-artifact@v1 91 | with: 92 | name: cross-apple-darwin 93 | path: /usr/local/bin/ 94 | - run: chmod +x /usr/local/bin/cross 95 | 96 | - run: ci/set_rust_version.bash stable ${{ matrix.target }} 97 | - run: ci/build.bash cross ${{ matrix.target }} RELEASE 98 | - run: tar -czvf ${{ env.BIN }}.tar.gz --directory=target/${{ matrix.target }}/release ${{ env.BIN }} 99 | - uses: XAMPPRocky/create-release@v1.0.2 100 | id: create_release 101 | env: 102 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 103 | with: 104 | tag_name: ${{ github.ref }} 105 | release_name: ${{ github.ref }} 106 | draft: false 107 | prerelease: false 108 | - uses: actions/upload-release-asset@v1 109 | id: upload-release-asset 110 | env: 111 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 112 | with: 113 | upload_url: ${{ steps.create_release.outputs.upload_url }} 114 | asset_path: ${{ env.BIN }}.tar.gz 115 | asset_name: ${{ env.BIN }}-${{ matrix.target }}.tar.gz 116 | asset_content_type: application/gzip 117 | 118 | linux: 119 | runs-on: ubuntu-latest 120 | needs: install-cross 121 | strategy: 122 | matrix: 123 | target: 124 | # WASM, off by default as most rust projects aren't compatible yet. 125 | # - wasm32-unknown-emscripten 126 | # Linux 127 | - i686-unknown-linux-gnu 128 | - i686-unknown-linux-musl 129 | - x86_64-unknown-linux-gnu 130 | - x86_64-unknown-linux-musl 131 | steps: 132 | - uses: actions/checkout@v2 133 | - uses: actions/download-artifact@v1 134 | with: 135 | name: cross-linux-musl 136 | path: /tmp/ 137 | - run: chmod +x /tmp/cross 138 | 139 | - run: ci/set_rust_version.bash stable ${{ matrix.target }} 140 | - run: ci/build.bash /tmp/cross ${{ matrix.target }} RELEASE 141 | - run: tar -czvf ${{ env.BIN }}.tar.gz --directory=target/${{ matrix.target }}/release ${{ env.BIN }} 142 | - uses: XAMPPRocky/create-release@v1.0.2 143 | id: create_release 144 | env: 145 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 146 | with: 147 | tag_name: ${{ github.ref }} 148 | release_name: ${{ github.ref }} 149 | draft: false 150 | prerelease: false 151 | - name: Upload Release Asset 152 | id: upload-release-asset 153 | uses: actions/upload-release-asset@v1 154 | env: 155 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 156 | with: 157 | upload_url: ${{ steps.create_release.outputs.upload_url }} 158 | asset_path: ${{ env.BIN }}.tar.gz 159 | asset_name: ${{ env.BIN }}-${{ matrix.target }}.tar.gz 160 | asset_content_type: application/gzip 161 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | 13 | #Added by cargo 14 | 15 | /target 16 | 17 | data/ 18 | 19 | request/target 20 | /.idea/ 21 | /.github/ 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "leetup" 3 | version = "1.2.6" 4 | authors = ["dragfire "] 5 | edition = "2018" 6 | description = "Leetcode cli" 7 | license = "MIT OR Apache-2.0" 8 | readme = "README.md" 9 | homepage = "https://github.com/dragfire/leetup" 10 | repository = "https://github.com/dragfire/leetup" 11 | keywords = ["cli", "leetcode"] 12 | categories = ["command-line-utilities"] 13 | exclude = [ 14 | "assets/*" 15 | ] 16 | 17 | [dependencies] 18 | leetup-cache = { path = "./cache", version = "0.2.0" } 19 | clap = "4.4.2" 20 | structopt = "0.3.15" 21 | thiserror = "1.0.20" 22 | anyhow = "1.0.31" 23 | serde = { version = "1.0", features = ["derive"] } 24 | serde_json = "1.0.55" 25 | ansi_term = "0.12.1" 26 | regex = "1.3.9" 27 | url = "2.1.1" 28 | cookie = "0.17.0" 29 | colci = "0.1.0" 30 | log = "0.4.11" 31 | env_logger = "0.10.0" 32 | html2text = "0.6.0" 33 | spinners = "1.2.0" 34 | dirs = "5.0.1" 35 | serde_repr = "0.1.6" 36 | shellexpand = "3.1.0" 37 | reqwest = { version = "0.11", features = ["json", "cookies"] } 38 | tokio = { version = "1", features = ["full"] } 39 | async-trait = "0.1.52" 40 | 41 | [dev-dependencies] 42 | tempfile = "3.1.0" 43 | predicates = "3.0.3" 44 | assert_cmd = "2.0.12" 45 | strip-ansi-escapes = "0.2.0" 46 | 47 | [target.x86_64-unknown-linux-gnu.dependencies] 48 | openssl = { version = "0.10", features = ["vendored"] } 49 | 50 | [target.x86_64-unknown-linux-musl.dependencies] 51 | openssl = { version = "0.10", features = ["vendored"] } 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Devajit Asem 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 | > If you use neovim, try this Neovim plugin [leetup.nvim](https://github.com/dragfire/leetup.nvim) 2 | 3 |

4 | 5 | [![crates](https://img.shields.io/crates/v/leetup.svg)](https://crates.io/crates/leetup) ![Downloads](https://img.shields.io/crates/d/leetup) [![CI](https://github.com/dragfire/leetup/actions/workflows/mean_bean_ci.yml/badge.svg)](https://github.com/dragfire/leetup/actions/workflows/mean_bean_ci.yml) 6 | 7 |

8 | 9 |

Solve Leetcode problems

10 | 11 | ![](assets/leetup.gif) 12 | 13 | ## Install 14 | - MacOS: 15 | ```sh 16 | brew install leetup 17 | ``` 18 | - Linux: 19 | Download from [releases](https://github.com/dragfire/leetup/releases). Extract the zipped file and set the PATH. 20 | - Cargo: 21 | ```sh 22 | cargo install leetup 23 | ``` 24 | - Windows: 25 | Download from [releases](https://github.com/dragfire/leetup/releases). Extract the zipped x86_64 windows target file. 26 | > Note: You will need to add `leetup.exe` to PATH to access from Command Prompt. 27 | 28 | ## Quick Start: 29 | - Login using Cookie: `leetup user -c` 30 | - You need to login on leetcode.com first. 31 | - Copy `csrftoken` and `LEETCODE_SESSION` from cookie storage in the browser. 32 | - Pick a problem: `leetup pick -l python 1` 33 | - Test a problem: 34 | `leetup test two-sum.py -t "[1,2]\n3"` 35 | or redirect test data using stdin 36 | ``` 37 | leetup test 3sum.java -t << END 38 | [1,-1,0] 39 | [0, 1, 1, 1, 2, -3, -1] 40 | [1,2,3] 41 | END 42 | ``` 43 | 44 | - Submit a problem: `leetup submit two-sum.py` 45 | - List/Show problems: `leetup list` 46 | - Search by keyword: `leetup list ` 47 | - Query easy: `leetup list -q e` 48 | - Order by Id, Title, Difficulty: `leetup list -qE -oIdT` 49 | - [More Commands](docs/usage.md) 50 | 51 | ## Inject code fragments: 52 | You can inject pieces of code that you frequently use in certain positions of the generated code file. Example: Standard library imports for each language can be put into a config. `Leetup` will pick it up and insert into the generated file. 53 | 54 | ### Config: 55 | Create `~/.leetup/config.json` and customize according to your preference: 56 | ```json 57 | { 58 | "lang": "java", 59 | "inject_code": { 60 | "rust": { 61 | "before_code": ["use std::rc::Rc;", "use std::collections::{HashMap, VecDeque};", "use std::cell::RefCell;"], 62 | "before_code_exclude": ["// Test comment", "// Test code"], 63 | "after_code": "\nstruct Solution; \n\nfn main() {\n let solution = Solution::$func();\n\n}\n", 64 | "before_function_definition": null 65 | }, 66 | "java": { 67 | "before_code": "import java.util.*;", 68 | "before_code_exclude": ["// Test comment", "// Test code"], 69 | "after_code": null, 70 | "before_function_definition": null 71 | }, 72 | "python3": { 73 | "before_code": "import math", 74 | "before_code_exclude": ["# Test comment", "# Test code"], 75 | "after_code": ["if __name__ = \"__main__\":", " solution = Solution()"], 76 | "before_function_definition": null 77 | } 78 | } 79 | } 80 | ``` 81 | Generated code looks something like this in Rust: 82 | ```rust 83 | // @leetup=custom 84 | // @leetup=info id=1 lang=rust slug=two-sum 85 | 86 | /* 87 | * [SNIP] 88 | */ 89 | // @leetup=custom 90 | 91 | // @leetup=inject:before_code_ex 92 | // Test comment 93 | // Test code 94 | // @leetup=inject:before_code_ex 95 | 96 | // @leetup=code 97 | 98 | // @leetup=inject:before_code 99 | use std::cell::RefCell; 100 | use std::collections::{HashMap, VecDeque}; 101 | use std::rc::Rc; 102 | // @leetup=inject:before_code 103 | 104 | impl Solution { 105 | pub fn two_sum(nums: Vec, target: i32) -> Vec {} 106 | } 107 | // @leetup=code 108 | 109 | // @leetup=inject:after_code 110 | // This is helpful when you want to run this program locally 111 | // and avoid writing this boilerplate code for each problem. 112 | struct Solution; 113 | 114 | fn main() { 115 | let solution = Solution::two_sum(); 116 | } 117 | 118 | // @leetup=inject:after_code 119 | ``` 120 | 121 | During testing and submitting to Leetcode, only the chunk of code between `@leetup=code` will be submitted: 122 | ```rust 123 | // @leetup=inject:before_code 124 | use std::cell::RefCell; 125 | use std::collections::{HashMap, VecDeque}; 126 | use std::rc::Rc; 127 | // @leetup=inject:before_code 128 | 129 | impl Solution { 130 | pub fn two_sum(nums: Vec, target: i32) -> Vec { 131 | } 132 | } 133 | ``` 134 | Others are ignored! 135 | 136 | ## Hook up script for Pick: 137 | Run scripts before/after code generation. It's useful when you want more ergonomics to move 138 | around the generated file e.g. create a directory, move the generated file to the directory, rename, etc. 139 | `@leetup=working_dir` will be replaced by `working_dir` in config. 140 | `@leetup=problem` will be replaced by the current problem tile e.g. `two-sum`. 141 | ```json 142 | { 143 | "lang": "rust", 144 | "inject_code": { 145 | ...SNIP... 146 | }, 147 | "pick_hook": { 148 | "rust": { 149 | "working_dir": "~/lc/rust", 150 | "script": { 151 | "pre_generation": ["cd @leetup=working_dir; mkdir -p @leetup=problem"], 152 | "post_generation": ["mv @leetup=working_dir/@leetup=problem.rs @leetup=working_dir/@leetup=problem/Solution.rs"] 153 | } 154 | }, 155 | "java": { 156 | "working_dir": "~/lc/java", 157 | "script": { 158 | "pre_generation": ["cd @leetup=working_dir", "mvn archetype:generate -DartifactId=@leetup=problem -DgroupId=leetup -DarchetypeGroupId=org.apache.maven.archetypes -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.4 -DinteractiveMode=false"], 159 | "post_generation": ["mv @leetup=working_dir/@leetup=problem.java @leetup=working_dir/@leetup=problem/src/main/java/App.java"] 160 | } 161 | } 162 | } 163 | } 164 | ``` 165 | 166 | ### Credit: 167 | This project is inspired by: https://github.com/leetcode-tools/leetcode-cli 168 | -------------------------------------------------------------------------------- /assets/leetup.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragfire/leetup/9f9f6418a87e4558a12c079fd14215bdb964501c/assets/leetup.gif -------------------------------------------------------------------------------- /assets/progress1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragfire/leetup/9f9f6418a87e4558a12c079fd14215bdb964501c/assets/progress1.png -------------------------------------------------------------------------------- /assets/progress2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragfire/leetup/9f9f6418a87e4558a12c079fd14215bdb964501c/assets/progress2.png -------------------------------------------------------------------------------- /assets/progress3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragfire/leetup/9f9f6418a87e4558a12c079fd14215bdb964501c/assets/progress3.png -------------------------------------------------------------------------------- /cache/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "leetup-cache" 3 | version = "0.2.0" 4 | authors = ["dragfire "] 5 | edition = "2018" 6 | description = "Cache" 7 | license = "MIT OR Apache-2.0" 8 | readme = "README.md" 9 | homepage = "https://github.com/dragfire/leetup" 10 | repository = "https://github.com/dragfire/leetup" 11 | keywords = ["cli", "leetcode"] 12 | categories = ["command-line-utilities"] 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [dependencies] 17 | anyhow = "1.0.31" 18 | serde = { version = "1.0", features = ["derive"] } 19 | serde_json = "1.0.55" 20 | -------------------------------------------------------------------------------- /cache/README.md: -------------------------------------------------------------------------------- 1 | # Cache 2 | Key-Value store 3 | -------------------------------------------------------------------------------- /cache/src/kvstore.rs: -------------------------------------------------------------------------------- 1 | use anyhow; 2 | use serde::{Deserialize, Serialize}; 3 | use serde_json::{self, Deserializer}; 4 | use std::collections::{BTreeMap, HashMap}; 5 | use std::ffi::OsStr; 6 | use std::fs::{self, File, OpenOptions}; 7 | use std::io::{self, BufReader, BufWriter, Read, Seek, SeekFrom, Write}; 8 | use std::ops::Range; 9 | use std::path::{Path, PathBuf}; 10 | 11 | pub type Result = anyhow::Result; 12 | 13 | // This constant is used for invoking log compaction 14 | const COMPACTION_THRESHOLD: u64 = 1024 * 1024; 15 | 16 | /// The `KvStore` stores string key/value pairs. 17 | /// 18 | /// Key/value pairs are persisted to disk in log files. Log files are named after 19 | /// monotonically increasing generation numbers with a `log` extension name. 20 | /// A `BTreeMap` in memory stores the keys and the value locations for fast query. 21 | /// 22 | /// ```rust 23 | /// # use yakv::{KvStore, Result}; 24 | /// # fn try_main() -> Result<()> { 25 | /// use std::env::current_dir; 26 | /// let mut store = KvStore::open(current_dir()?)?; 27 | /// store.set("key".to_owned(), "value".to_owned())?; 28 | /// let val = store.get("key".to_owne())?; 29 | /// assert_eq!(val, Some("value".to_owned())); 30 | /// # Ok(()) 31 | /// # } 32 | /// ``` 33 | pub struct KvStore { 34 | path: PathBuf, 35 | current_id: u64, 36 | writer: BufWriterWithPos, 37 | readers: HashMap>, 38 | index: BTreeMap, 39 | stale_data: u64, 40 | } 41 | 42 | impl KvStore { 43 | /// Opens a KvStore with the given path. 44 | pub fn open>(path: T) -> Result { 45 | // try to load all log files in the given path 46 | // if it failed then create a log file with an id suffix-ed to the file 47 | // e.g. key-1.log, key-2.log, key-3.log, etc 48 | // after loading all the logs, build the index in-memory 49 | let path = path.into(); 50 | fs::create_dir_all(&path)?; 51 | 52 | let mut readers = HashMap::new(); 53 | let mut index = BTreeMap::new(); 54 | let mut stale_data = 0; 55 | 56 | let ids = sorted_ids(&path)?; 57 | // println!("IDS: {:?}", ids); 58 | for &id in &ids { 59 | let mut reader = BufReaderWithPos::new(File::open(log_path(&path, id))?)?; 60 | stale_data += load_log(id, &mut reader, &mut index)?; 61 | readers.insert(id, reader); 62 | } 63 | 64 | let current_id = ids.last().unwrap_or(&0) + 1; 65 | let writer = create_log_file(current_id, &path, &mut readers)?; 66 | 67 | Ok(KvStore { 68 | path, 69 | current_id, 70 | writer, 71 | readers, 72 | index, 73 | stale_data, 74 | }) 75 | } 76 | 77 | /// Sets the value of s string key to a string. 78 | pub fn set(&mut self, key: String, value: String) -> Result<()> { 79 | let cmd = Command::set(key, value); 80 | let pos = self.writer.pos; 81 | serde_json::to_writer(&mut self.writer, &cmd)?; 82 | self.writer.flush()?; 83 | 84 | if let Command::Set { key, .. } = cmd { 85 | if let Some(old_cmd) = self.index.insert( 86 | key, 87 | CommandPos::from((self.current_id, pos..self.writer.pos)), 88 | ) { 89 | self.stale_data += old_cmd.len; 90 | } 91 | } 92 | 93 | // Handle log compaction 94 | if self.stale_data > COMPACTION_THRESHOLD { 95 | self.compact()?; 96 | } 97 | 98 | Ok(()) 99 | } 100 | 101 | /// Gets the string value for a given key. 102 | pub fn get(&mut self, key: String) -> Result> { 103 | // println!("{:?}", self.index); 104 | if let Some(cmd_pos) = self.index.get(&key) { 105 | let reader = self 106 | .readers 107 | .get_mut(&cmd_pos.id) 108 | .expect("Cannot find reader"); 109 | 110 | reader.seek(SeekFrom::Start(cmd_pos.pos))?; 111 | let cmd_reader = reader.take(cmd_pos.len); 112 | if let Command::Set { value, .. } = serde_json::from_reader(cmd_reader)? { 113 | return Ok(Some(value)); 114 | } else { 115 | return Err(anyhow::Error::msg("Unexpected command")); 116 | } 117 | } 118 | Ok(None) 119 | } 120 | 121 | /// Check if key exists in the cache 122 | pub fn has_key(&self, key: String) -> bool { 123 | self.index.contains_key(&key) 124 | } 125 | 126 | /// Removes the given key. 127 | pub fn remove(&mut self, key: String) -> Result<()> { 128 | // check if key exist in index and delete if from the log file 129 | if self.index.contains_key(&key) { 130 | let cmd = Command::remove(key.to_owned()); 131 | serde_json::to_writer(&mut self.writer, &cmd)?; 132 | self.writer.flush()?; 133 | let old_cmd = self.index.remove(&key).expect("Key not found"); 134 | self.stale_data += old_cmd.len; 135 | Ok(()) 136 | } else { 137 | Err(anyhow::Error::msg("Key not found")) 138 | } 139 | } 140 | 141 | fn compact(&mut self) -> Result<()> { 142 | // increment id by 1 143 | // this will be used by compaction writer 144 | let compaction_id = self.current_id + 1; 145 | self.current_id += 2; 146 | self.writer = create_log_file(self.current_id, &self.path, &mut self.readers)?; 147 | let mut compaction_writer = create_log_file(compaction_id, &self.path, &mut self.readers)?; 148 | 149 | let mut new_pos = 0; 150 | for cmd_pos in &mut self.index.values_mut() { 151 | let cmd_reader = self.readers.get_mut(&cmd_pos.id).expect("reader not found"); 152 | if cmd_reader.pos != cmd_pos.pos { 153 | cmd_reader.seek(SeekFrom::Start(cmd_pos.pos))?; 154 | } 155 | 156 | let mut cmd_reader = cmd_reader.take(cmd_pos.len); 157 | let len = io::copy(&mut cmd_reader, &mut compaction_writer)?; 158 | *cmd_pos = CommandPos::from((compaction_id, new_pos..new_pos + len)); 159 | new_pos += len; 160 | } 161 | compaction_writer.flush()?; 162 | 163 | let stale_ids: Vec<_> = self 164 | .readers 165 | .keys() 166 | .filter(|id| id < &&compaction_id) 167 | .cloned() 168 | .collect(); 169 | 170 | for stale_id in stale_ids { 171 | self.readers.remove(&stale_id); 172 | fs::remove_file(log_path(&self.path, stale_id))?; 173 | } 174 | self.stale_data = 0; 175 | 176 | Ok(()) 177 | } 178 | } 179 | 180 | fn log_path>(path: T, id: u64) -> PathBuf { 181 | path.as_ref().join(format!("{}.log", id)) 182 | } 183 | 184 | fn create_log_file( 185 | id: u64, 186 | path: &Path, 187 | readers: &mut HashMap>, 188 | ) -> Result> { 189 | let path = log_path(&path, id); 190 | let writer = BufWriterWithPos::new(OpenOptions::new().create(true).append(true).open(&path)?)?; 191 | readers.insert(id, BufReaderWithPos::new(File::open(&path)?)?); 192 | Ok(writer) 193 | } 194 | 195 | // load a log and build index 196 | fn load_log( 197 | id: u64, 198 | reader: &mut BufReaderWithPos, 199 | index: &mut BTreeMap, 200 | ) -> Result { 201 | let mut pos = reader.seek(SeekFrom::Start(0))?; 202 | let mut stream = Deserializer::from_reader(reader).into_iter::(); 203 | let mut stale_data = 0; 204 | // println!("ID: {}", id); 205 | while let Some(cmd) = stream.next() { 206 | let new_pos = stream.byte_offset() as u64; 207 | match cmd? { 208 | Command::Set { key, .. } => { 209 | if let Some(old_cmd) = index.insert(key, CommandPos::from((id, pos..new_pos))) { 210 | stale_data += old_cmd.len; 211 | } 212 | } 213 | Command::Remove { key } => { 214 | if let Some(old_cmd) = index.remove(&key) { 215 | stale_data += old_cmd.len; 216 | } 217 | 218 | stale_data += new_pos - pos; 219 | } 220 | } 221 | pos = new_pos; 222 | } 223 | Ok(stale_data) 224 | } 225 | 226 | // get all ids from the log files in a given path 227 | // 228 | // Returns sorted id numbers 229 | fn sorted_ids(path: &Path) -> Result> { 230 | let mut ids: Vec = fs::read_dir(&path)? 231 | .flat_map(|dir_entry| -> Result<_> { Ok(dir_entry?.path()) }) 232 | .filter(|path| path.is_file() && path.extension() == Some("log".as_ref())) 233 | .filter_map(|path| { 234 | path.file_name() 235 | .and_then(OsStr::to_str) 236 | .map(|s| s.trim_end_matches(".log")) 237 | .map(str::parse::) 238 | }) 239 | .flatten() 240 | .collect(); 241 | ids.sort(); 242 | Ok(ids) 243 | } 244 | 245 | pub struct BufReaderWithPos { 246 | reader: BufReader, 247 | pos: u64, 248 | } 249 | 250 | impl BufReaderWithPos { 251 | fn new(mut file: T) -> Result { 252 | let pos = file.seek(SeekFrom::Current(0))?; 253 | Ok(BufReaderWithPos { 254 | reader: BufReader::new(file), 255 | pos, 256 | }) 257 | } 258 | } 259 | 260 | impl Read for BufReaderWithPos { 261 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 262 | let len = self.reader.read(buf)?; 263 | self.pos += len as u64; 264 | Ok(len) 265 | } 266 | } 267 | 268 | impl Seek for BufReaderWithPos { 269 | fn seek(&mut self, pos: SeekFrom) -> io::Result { 270 | self.pos = self.reader.seek(pos)?; 271 | Ok(self.pos) 272 | } 273 | } 274 | 275 | struct BufWriterWithPos { 276 | writer: BufWriter, 277 | pos: u64, 278 | } 279 | 280 | impl BufWriterWithPos { 281 | fn new(mut file: T) -> Result { 282 | let pos = file.seek(SeekFrom::Current(0))?; 283 | Ok(BufWriterWithPos { 284 | writer: BufWriter::new(file), 285 | pos, 286 | }) 287 | } 288 | } 289 | 290 | impl Write for BufWriterWithPos { 291 | fn write(&mut self, buf: &[u8]) -> io::Result { 292 | let len = self.writer.write(buf)?; 293 | self.pos += len as u64; 294 | Ok(len) 295 | } 296 | 297 | fn flush(&mut self) -> io::Result<()> { 298 | self.writer.flush() 299 | } 300 | } 301 | 302 | impl Seek for BufWriterWithPos { 303 | fn seek(&mut self, pos: SeekFrom) -> io::Result { 304 | self.pos = self.writer.seek(pos)?; 305 | Ok(self.pos) 306 | } 307 | } 308 | 309 | /// Represent KV store commands 310 | #[derive(Serialize, Deserialize, Debug)] 311 | enum Command { 312 | Set { key: String, value: String }, 313 | Remove { key: String }, 314 | } 315 | 316 | impl Command { 317 | fn set(key: String, value: String) -> Self { 318 | Command::Set { key, value } 319 | } 320 | 321 | fn remove(key: String) -> Self { 322 | Command::Remove { key } 323 | } 324 | } 325 | 326 | /// Position for Command in log file 327 | /// 328 | /// Stores log file id, offset, and length 329 | #[derive(Debug)] 330 | struct CommandPos { 331 | id: u64, 332 | pos: u64, 333 | len: u64, 334 | } 335 | 336 | impl From<(u64, Range)> for CommandPos { 337 | fn from((id, range): (u64, Range)) -> Self { 338 | CommandPos { 339 | id, 340 | pos: range.start, 341 | len: range.end - range.start, 342 | } 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /cache/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod kvstore; 2 | -------------------------------------------------------------------------------- /ci/build.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Script for building your rust projects. 3 | set -e 4 | 5 | source ci/common.bash 6 | 7 | # $1 {path} = Path to cross/cargo executable 8 | CROSS=$1 9 | # $1 {string} = e.g. x86_64-pc-windows-msvc 10 | TARGET_TRIPLE=$2 11 | # $3 {boolean} = Are we building for deployment? 12 | RELEASE_BUILD=$3 13 | 14 | required_arg $CROSS 'CROSS' 15 | required_arg $TARGET_TRIPLE '' 16 | 17 | if [ -z "$RELEASE_BUILD" ]; then 18 | $CROSS build --target $TARGET_TRIPLE 19 | $CROSS build --target $TARGET_TRIPLE --all-features 20 | else 21 | $CROSS build --target $TARGET_TRIPLE --all-features --release 22 | fi 23 | 24 | -------------------------------------------------------------------------------- /ci/common.bash: -------------------------------------------------------------------------------- 1 | required_arg() { 2 | if [ -z "$1" ]; then 3 | echo "Required argument $2 missing" 4 | exit 1 5 | fi 6 | } 7 | -------------------------------------------------------------------------------- /ci/set_rust_version.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | rustup default $1 4 | rustup target add $2 5 | -------------------------------------------------------------------------------- /ci/test.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Script for building your rust projects. 3 | set -e 4 | 5 | source ci/common.bash 6 | 7 | # $1 {path} = Path to cross/cargo executable 8 | CROSS=$1 9 | # $1 {string} = 10 | TARGET_TRIPLE=$2 11 | 12 | required_arg $CROSS 'CROSS' 13 | required_arg $TARGET_TRIPLE '' 14 | 15 | $CROSS test --target $TARGET_TRIPLE 16 | $CROSS test --target $TARGET_TRIPLE --all-features 17 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | ## Help 2 | ```markdown 3 | ❯ leetup --help 4 | 5 | USAGE: 6 | leetup 7 | 8 | FLAGS: 9 | -h, --help Prints help information 10 | -V, --version Prints version information 11 | 12 | SUBCOMMANDS: 13 | help Prints this message or the help of the given subcommand(s) 14 | list List questions 15 | pick Pick a problem 16 | submit Submit a problem 17 | test Submit a problem 18 | user User auth 19 | ``` 20 | 21 | ## List 22 | ```markdown 23 | ❯ leetup list --help 24 | 25 | List questions 26 | 27 | USAGE: 28 | leetup list [FLAGS] [OPTIONS] [keyword] 29 | 30 | FLAGS: 31 | -h, --help Prints help information 32 | -s, --stat Show statistic counter of the output list 33 | -V, --version Prints version information 34 | 35 | OPTIONS: 36 | -o, --order Order by ProblemId, Question Title, or Difficulty 37 | -q, --query Query by conditions 38 | -t, --tag Filter by given tag 39 | 40 | ARGS: 41 | 42 | ``` 43 | 44 | ## Pick 45 | ```markdown 46 | ❯ leetup pick --help 47 | 48 | Pick a problem 49 | 50 | USAGE: 51 | leetup pick [FLAGS] [OPTIONS] [id] 52 | 53 | FLAGS: 54 | -d Include problem definition in generated source file 55 | -g Generate code if true 56 | -h, --help Prints help information 57 | -V, --version Prints version information 58 | 59 | OPTIONS: 60 | -l, --lang Language used to generate problem's source [default: rust] 61 | 62 | ARGS: 63 | Show/Pick a problem using ID 64 | ``` 65 | 66 | ## Submit 67 | ```markdown 68 | ❯ leetup submit --help 69 | 70 | Submit a problem 71 | 72 | USAGE: 73 | leetup submit 74 | 75 | FLAGS: 76 | -h, --help Prints help information 77 | -V, --version Prints version information 78 | 79 | ARGS: 80 | Code filename 81 | ``` 82 | 83 | ## Test 84 | ```markdown 85 | ❯ leetup test --help 86 | 87 | Test a problem 88 | 89 | USAGE: 90 | leetup test -t 91 | 92 | FLAGS: 93 | -h, --help Prints help information 94 | -V, --version Prints version information 95 | 96 | OPTIONS: 97 | -t Custom test cases 98 | 99 | ARGS: 100 | Code filename 101 | ``` 102 | 103 | ## User 104 | ```markdown 105 | ❯ leetup user --help 106 | 107 | User auth 108 | 109 | USAGE: 110 | leetup user [OPTIONS] 111 | 112 | FLAGS: 113 | -h, --help Prints help information 114 | -V, --version Prints version information 115 | 116 | OPTIONS: 117 | -c, --cookie Login using cookie 118 | -g, --github Login using github 119 | -l, --logout Logout user 120 | ``` 121 | -------------------------------------------------------------------------------- /src/client.rs: -------------------------------------------------------------------------------- 1 | use crate::{service::Session, Config, LeetUpError, Result}; 2 | use anyhow::anyhow; 3 | use log::debug; 4 | use reqwest::{header, header::HeaderMap, header::HeaderValue, Client, Response}; 5 | 6 | pub struct RemoteClient<'a> { 7 | config: &'a Config, 8 | session: Option<&'a Session>, 9 | } 10 | 11 | impl<'a> RemoteClient<'_> { 12 | pub fn new(config: &'a Config, session: Option<&'a Session>) -> RemoteClient<'a> { 13 | RemoteClient { config, session } 14 | } 15 | 16 | /// Make a GET request 17 | pub async fn get( 18 | &self, 19 | url: &str, 20 | headers_opt: Option, 21 | session: Option<&Session>, 22 | ) -> Result { 23 | let headers = self.headers_with_session(headers_opt, session); 24 | let client = Client::builder().default_headers(headers).build()?; 25 | client.get(url).send().await.map_err(LeetUpError::Reqwest) 26 | } 27 | 28 | /// Make a POST request 29 | pub async fn post( 30 | &self, 31 | url: &str, 32 | body: &T, 33 | with_headers: F, 34 | ) -> Result 35 | where 36 | F: FnOnce() -> Option, 37 | { 38 | let headers = self.headers_with_session(with_headers(), self.session); 39 | debug!("Headers: {:#?}", headers); 40 | let client = Client::builder().default_headers(headers).build()?; 41 | 42 | let client = client 43 | .post(url) 44 | .header( 45 | header::ORIGIN, 46 | HeaderValue::from_str(&self.config.urls.base).unwrap(), 47 | ) 48 | .json(body); 49 | 50 | let res = client.send().await?; 51 | 52 | if res.status() == 200 { 53 | res.json::() 54 | .await 55 | .map_err(|e| e.into()) 56 | } else { 57 | Err(LeetUpError::Any(anyhow!("Status: {}", res.status()))) 58 | } 59 | } 60 | 61 | fn headers_with_session( 62 | &self, 63 | headers_opt: Option, 64 | session: Option<&Session>, 65 | ) -> HeaderMap { 66 | let mut headers = headers_opt.unwrap_or_default(); 67 | 68 | if let Some(session) = session { 69 | let cookie: String = session.into(); 70 | headers.insert("Cookie", HeaderValue::from_str(&cookie).unwrap()); 71 | headers.insert("X-CSRFToken", HeaderValue::from_str(&session.csrf).unwrap()); 72 | headers.insert( 73 | "X-Requested-With", 74 | HeaderValue::from_static("XMLHttpRequest"), 75 | ); 76 | } 77 | 78 | headers 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/cmd.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use leetup_cache::kvstore::KvStore; 4 | use log::debug; 5 | use spinners::{Spinner, Spinners}; 6 | use structopt::StructOpt; 7 | 8 | use crate::service::{CacheKey, Session}; 9 | use crate::{ 10 | service::{leetcode::Leetcode, Lang, ServiceProvider}, 11 | Config, Result, 12 | }; 13 | 14 | #[derive(Debug, StructOpt)] 15 | pub struct List { 16 | pub keyword: Option, 17 | 18 | /// Filter by given tag 19 | #[structopt(short, long)] 20 | pub tag: Option, 21 | 22 | /// Query by conditions 23 | #[structopt(short, long)] 24 | pub query: Option, 25 | 26 | /// Show statistic counter of the output list 27 | #[structopt(short, long)] 28 | pub stat: bool, 29 | 30 | /// Order by ProblemId, Question Title, or Difficulty 31 | #[structopt(short, long)] 32 | pub order: Option, 33 | } 34 | 35 | #[derive(Debug, StructOpt)] 36 | pub struct User { 37 | /// Login using cookie 38 | #[structopt(short, long)] 39 | pub cookie: Option>, 40 | 41 | /// Logout user 42 | #[structopt(short, long)] 43 | pub logout: Option>, 44 | } 45 | 46 | #[derive(Debug, StructOpt)] 47 | pub struct Pick { 48 | /// Show/Pick a problem using ID. 49 | pub id: Option, 50 | 51 | /// Generate code if true. 52 | #[structopt(short)] 53 | pub generate: bool, 54 | 55 | /// Include problem definition in generated source file. 56 | #[structopt(short)] 57 | pub def: bool, 58 | 59 | /// Language used to generate problem's source. 60 | #[structopt(short, long)] 61 | pub lang: Option, 62 | } 63 | 64 | #[derive(Debug, StructOpt)] 65 | pub struct Submit { 66 | /// Code filename. 67 | pub filename: String, 68 | } 69 | 70 | #[derive(Debug, StructOpt)] 71 | pub struct Test { 72 | /// Code filename. 73 | pub filename: String, 74 | 75 | /// Custom test cases. 76 | #[structopt(short)] 77 | pub test_data: Option>, 78 | } 79 | 80 | #[derive(Debug, StructOpt)] 81 | pub enum Command { 82 | /// List questions 83 | #[structopt(name = "list")] 84 | List(List), 85 | 86 | /// User auth 87 | #[structopt(name = "user")] 88 | User(User), 89 | 90 | /// Pick a problem 91 | #[structopt(name = "pick")] 92 | Pick(Pick), 93 | 94 | /// Submit a problem 95 | #[structopt(name = "submit")] 96 | Submit(Submit), 97 | 98 | /// Test a problem 99 | #[structopt(name = "test")] 100 | Test(Test), 101 | } 102 | 103 | /// -q to query by conditions. 104 | /// e = easy, E = not easy = m + h. 105 | /// m = medium, M = not medium = e + h. 106 | /// h = hard, H = not hard = e + m. 107 | /// d = done = AC-ed, D = not AC-ed. 108 | /// l = locked, L = not locked. 109 | /// s = starred, S = unstarred. 110 | #[derive(Debug)] 111 | pub enum Query { 112 | Easy = 1, 113 | Medium, 114 | Hard, 115 | NotEasy, 116 | NotMedium, 117 | NotHard, 118 | Locked, 119 | Unlocked, 120 | Done, 121 | NotDone, 122 | Starred, 123 | Unstarred, 124 | } 125 | 126 | impl From for Query { 127 | fn from(c: char) -> Self { 128 | match c { 129 | 'e' => Query::Easy, 130 | 'E' => Query::NotEasy, 131 | 'm' => Query::Medium, 132 | 'M' => Query::NotMedium, 133 | 'h' => Query::Hard, 134 | 'H' => Query::NotHard, 135 | 'l' => Query::Locked, 136 | 'L' => Query::Unlocked, 137 | 'd' => Query::Done, 138 | 'D' => Query::NotDone, 139 | 's' => Query::Starred, 140 | 'S' => Query::Unstarred, 141 | _ => Query::Easy, 142 | } 143 | } 144 | } 145 | 146 | impl Query { 147 | pub fn from_str(q: &str) -> Vec { 148 | q.chars().map(Query::from).collect() 149 | } 150 | } 151 | 152 | pub enum OrderBy { 153 | /// Order by question Id in Ascending order 154 | IdAsc, 155 | 156 | /// Order by question Id in Descending order 157 | IdDesc, 158 | TitleAsc, 159 | TitleDesc, 160 | DifficultyAsc, 161 | DifficultyDesc, 162 | } 163 | 164 | impl From for OrderBy { 165 | fn from(c: char) -> Self { 166 | match c { 167 | 'i' => OrderBy::IdAsc, 168 | 'I' => OrderBy::IdDesc, 169 | 't' => OrderBy::TitleAsc, 170 | 'T' => OrderBy::TitleDesc, 171 | 'd' => OrderBy::DifficultyAsc, 172 | 'D' => OrderBy::DifficultyDesc, 173 | _ => OrderBy::IdAsc, 174 | } 175 | } 176 | } 177 | 178 | impl OrderBy { 179 | pub fn from_str(order: &str) -> Vec { 180 | order.chars().map(OrderBy::from).collect() 181 | } 182 | } 183 | 184 | #[derive(StructOpt, Debug)] 185 | #[structopt(name = "leetup")] 186 | pub struct LeetUpArgs { 187 | #[structopt(subcommand)] 188 | pub command: Command, 189 | } 190 | 191 | pub async fn process() -> Result<()> { 192 | let opt = LeetUpArgs::from_args(); 193 | debug!("Options: {:#?}", opt); 194 | 195 | let config_dir = create_config_directory()?; 196 | let mut cache = KvStore::open(&config_dir)?; 197 | let session = get_session(&mut cache)?; 198 | let config = get_config(config_dir); 199 | debug!("Session: {:#?}", session); 200 | debug!("Config: {:#?}", config); 201 | 202 | let mut provider = Leetcode::new(session.as_ref(), &config, cache)?; 203 | 204 | match opt.command { 205 | Command::Pick(pick) => { 206 | provider.pick_problem(pick).await?; 207 | } 208 | Command::List(list) => { 209 | provider.list_problems(list).await?; 210 | } 211 | Command::User(user) => { 212 | provider.process_auth(user).await?; 213 | } 214 | Command::Submit(submit) => { 215 | let sp = Spinner::new(Spinners::Dots9, "Waiting for judge result!".into()); 216 | provider.problem_submit(submit).await?; 217 | sp.stop(); 218 | } 219 | Command::Test(test) => { 220 | let sp = Spinner::new(Spinners::Dots9, "Waiting for judge result!".into()); 221 | provider.problem_test(test).await?; 222 | sp.stop(); 223 | } 224 | } 225 | Ok(()) 226 | } 227 | 228 | fn get_config(mut config_dir: PathBuf) -> Config { 229 | config_dir.push("config.json"); 230 | Config::get(config_dir) 231 | } 232 | 233 | fn get_session(cache: &mut KvStore) -> Result> { 234 | let mut session: Option = None; 235 | let session_val = cache.get(CacheKey::Session.into())?; 236 | 237 | // Set session if the user is logged in 238 | if let Some(ref val) = session_val { 239 | session = Some(serde_json::from_str::(val)?); 240 | } 241 | Ok(session) 242 | } 243 | 244 | fn create_config_directory() -> Result { 245 | // create .leetup directory: ~/.leetup/*.log 246 | let mut data_dir = PathBuf::new(); 247 | data_dir.push( 248 | dirs::home_dir() 249 | .ok_or("Home directory not available!") 250 | .map_err(anyhow::Error::msg)?, 251 | ); 252 | data_dir.push(".leetup"); 253 | 254 | Ok(data_dir) 255 | } 256 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::Read; 3 | use std::path::Path; 4 | use std::{collections::HashMap, str::FromStr}; 5 | 6 | use log::warn; 7 | use serde::{de::DeserializeOwned, Deserialize}; 8 | 9 | use crate::{service::Lang, LeetUpError, Result}; 10 | 11 | type LangInjectCode = HashMap; 12 | type PickHookConfig = HashMap; 13 | 14 | #[derive(Debug, Deserialize)] 15 | pub struct Config { 16 | #[serde(skip)] 17 | pub urls: Urls, 18 | pub inject_code: Option, 19 | pub pick_hook: Option, 20 | pub lang: Lang, 21 | } 22 | 23 | impl Config { 24 | pub fn get>(path: P) -> Self { 25 | let base = "https://leetcode.com"; 26 | let urls = Urls { 27 | base: base.to_owned(), 28 | api: format!("{}/api", base), 29 | graphql: format!("{}/graphql", base), 30 | problems: format!("{}/problems/", base), 31 | problems_all: format!("{}/api/problems/all", base), 32 | github_login: format!("{}/accounts/github/login/?next=%2F", base), 33 | github_login_request: "https://github.com/login".to_string(), 34 | github_session_request: "https://github.com/session".to_string(), 35 | test: format!("{}/problems/$slug/interpret_solution/", base), 36 | submit: format!("{}/problems/$slug/submit/", base), 37 | submissions: format!("{}/api/submissions/$slug", base), 38 | submission: format!("{}/submissions/detail/$id", base), 39 | verify: format!("{}/submissions/detail/$id/check/", base), 40 | }; 41 | 42 | let config: Result = Config::get_config(path); 43 | 44 | match config { 45 | Ok(mut c) => { 46 | c.urls = urls.clone(); 47 | c 48 | } 49 | Err(e) => { 50 | warn!("{:#?}", e); 51 | Config { 52 | urls, 53 | inject_code: None, 54 | pick_hook: None, 55 | lang: Lang::from_str("rust").unwrap(), 56 | } 57 | } 58 | } 59 | } 60 | 61 | fn get_config, T: DeserializeOwned>(path: P) -> Result { 62 | let mut buf = String::new(); 63 | let mut file = File::open(path)?; 64 | file.read_to_string(&mut buf)?; 65 | 66 | serde_json::from_str(&buf).map_err(LeetUpError::Serde) 67 | } 68 | } 69 | 70 | #[derive(Deserialize, Debug)] 71 | #[serde(untagged)] 72 | pub enum Either { 73 | Sequence(Vec), 74 | String(String), 75 | } 76 | 77 | impl From for Either { 78 | fn from(s: String) -> Self { 79 | let split: Vec = s.trim().split("\n").map(|st| st.to_owned()).collect(); 80 | if split.is_empty() { 81 | Either::String(s.to_owned()) 82 | } else { 83 | Either::Sequence(split) 84 | } 85 | } 86 | } 87 | 88 | impl ToString for Either { 89 | fn to_string(&self) -> String { 90 | match self { 91 | Either::String(s) => s.to_owned(), 92 | Either::Sequence(v) => v.join("\n"), 93 | } 94 | } 95 | } 96 | 97 | #[derive(Debug, Default, Deserialize, Clone)] 98 | pub struct Urls { 99 | pub base: String, 100 | pub api: String, 101 | pub graphql: String, 102 | pub problems: String, 103 | pub problems_all: String, 104 | pub github_login: String, 105 | pub github_login_request: String, 106 | pub github_session_request: String, 107 | pub test: String, 108 | pub submit: String, 109 | pub submissions: String, 110 | pub submission: String, 111 | pub verify: String, 112 | } 113 | 114 | #[derive(Debug, Deserialize)] 115 | pub struct InjectCode { 116 | pub before_code: Option, 117 | pub before_code_exclude: Option, 118 | pub after_code: Option, 119 | pub before_function_definition: Option, 120 | } 121 | 122 | /// Make code generation more flexible with capabilities to run scripts before 123 | /// and after generation. 124 | /// 125 | /// Provide the ability to change filenames through certain pre-defined transformation actions. 126 | #[derive(Debug, Deserialize)] 127 | pub struct PickHook { 128 | working_dir: Option, 129 | script: Option, 130 | } 131 | 132 | impl PickHook { 133 | pub fn working_dir(&self) -> Option<&str> { 134 | self.working_dir.as_ref().map(String::as_ref) 135 | } 136 | 137 | pub fn script_pre_generation(&self) -> Option<&Either> { 138 | match self.script.as_ref() { 139 | Some(script) => script.pre_generation.as_ref(), 140 | None => None, 141 | } 142 | } 143 | 144 | pub fn script_post_generation(&self) -> Option<&Either> { 145 | match self.script.as_ref() { 146 | Some(script) => script.post_generation.as_ref(), 147 | None => None, 148 | } 149 | } 150 | } 151 | 152 | #[derive(Debug, Deserialize)] 153 | pub struct PickHookScript { 154 | pre_generation: Option, 155 | post_generation: Option, 156 | } 157 | 158 | #[test] 159 | fn test_config() { 160 | use std::io::Write; 161 | 162 | let data_dir = tempfile::tempdir().unwrap(); 163 | let data = serde_json::json!({ 164 | "inject_code": {}, 165 | "urls": { 166 | "base": vec![""] 167 | }, 168 | "pick_hook": {}, 169 | "lang": "java" 170 | }); 171 | let file_path = data_dir.path().join("config.json"); 172 | 173 | let mut file = std::fs::File::create(&file_path).unwrap(); 174 | file.write(data.to_string().as_bytes()).unwrap(); 175 | 176 | let config: Config = Config::get(&file_path); 177 | assert!(config.inject_code.is_some()); 178 | assert!(!config.urls.base.is_empty()); 179 | assert!(config.pick_hook.is_some()); 180 | assert!(matches!(config.lang, Lang::Java(..))); 181 | drop(file); 182 | data_dir.close().unwrap(); 183 | } 184 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use thiserror::Error; 4 | 5 | /// Represent all LeetUp error 6 | #[derive(Error, Debug)] 7 | #[error("{0}")] 8 | pub enum LeetUpError { 9 | /// Any Error 10 | Any(#[from] anyhow::Error), 11 | 12 | /// IO Error 13 | Io(#[from] io::Error), 14 | 15 | /// Serde Error 16 | Serde(#[from] serde_json::Error), 17 | 18 | /// Regex Error 19 | Regex(#[from] regex::Error), 20 | 21 | /// Reqwest Error 22 | Reqwest(#[from] reqwest::Error), 23 | 24 | /// Invalid header value error 25 | InvalidHeaderValue(#[from] reqwest::header::InvalidHeaderValue), 26 | 27 | /// Option None Error 28 | #[error("Tried to unwrap None")] 29 | OptNone, 30 | 31 | /// Unexpected Command Error 32 | #[error("Unexpected command")] 33 | UnexpectedCommand, 34 | } 35 | 36 | /// Handle Result 37 | pub type Result = anyhow::Result; 38 | -------------------------------------------------------------------------------- /src/icon.rs: -------------------------------------------------------------------------------- 1 | pub enum Icon { 2 | Yes, 3 | _No, 4 | Star, 5 | _Unstar, 6 | Lock, 7 | Empty, 8 | } 9 | 10 | impl ToString for Icon { 11 | fn to_string(&self) -> String { 12 | match self { 13 | Icon::Yes => "✔".to_string(), 14 | Icon::_No => "✘".to_string(), 15 | Icon::Star => "★".to_string(), 16 | Icon::_Unstar => "☆".to_string(), 17 | Icon::Lock => "🔒".to_string(), 18 | Icon::Empty => " ".to_string(), 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use config::*; 2 | pub use error::{LeetUpError, Result}; 3 | 4 | pub mod cmd; 5 | mod config; 6 | mod error; 7 | mod printer; 8 | 9 | pub(crate) mod client; 10 | pub(crate) mod icon; 11 | pub(crate) mod model; 12 | pub(crate) mod service; 13 | pub(crate) mod template; 14 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use leetup::{cmd, Result}; 2 | 3 | #[tokio::main] 4 | async fn main() -> Result<()> { 5 | env_logger::init(); 6 | cmd::process().await?; 7 | Ok(()) 8 | } 9 | -------------------------------------------------------------------------------- /src/model/mod.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | use std::str::FromStr; 3 | 4 | use ansi_term::Color::{Green, Red, Yellow}; 5 | use serde::Deserialize; 6 | use serde_repr::{Deserialize_repr, Serialize_repr}; 7 | 8 | use DifficultyType::*; 9 | 10 | use crate::{Either, LeetUpError}; 11 | 12 | #[derive(Debug)] 13 | pub struct Problem { 14 | pub id: usize, 15 | pub slug: String, 16 | pub lang: String, 17 | pub link: String, 18 | pub typed_code: Option, 19 | } 20 | 21 | #[derive(Ord, PartialOrd, Eq, PartialEq, Clone, Serialize_repr, Deserialize_repr, Debug)] 22 | #[repr(u8)] 23 | pub enum DifficultyType { 24 | Easy = 1, 25 | Medium, 26 | Hard, 27 | } 28 | 29 | impl FromStr for DifficultyType { 30 | type Err = LeetUpError; 31 | 32 | fn from_str(s: &str) -> std::result::Result { 33 | let easy = Easy.to_string(); 34 | let medium = Medium.to_string(); 35 | let hard = Hard.to_string(); 36 | match s { 37 | x if x == easy => Ok(Easy), 38 | x if x == medium => Ok(Medium), 39 | x if x == hard => Ok(Hard), 40 | _ => Err(LeetUpError::UnexpectedCommand), 41 | } 42 | } 43 | } 44 | 45 | impl ToString for DifficultyType { 46 | fn to_string(&self) -> String { 47 | match self { 48 | Easy => "Easy".into(), 49 | Medium => "Medium".into(), 50 | Hard => "Hard".into(), 51 | } 52 | } 53 | } 54 | 55 | #[derive(Deserialize, Debug)] 56 | #[serde(untagged)] 57 | pub enum Difficulty { 58 | Cardinal { level: DifficultyType }, 59 | String(String), 60 | } 61 | 62 | impl<'a> From<&'_ Difficulty> for DifficultyType { 63 | fn from(difficulty: &Difficulty) -> Self { 64 | match difficulty { 65 | Difficulty::Cardinal { level } => level.clone(), 66 | Difficulty::String(s) => DifficultyType::from_str(s).unwrap(), 67 | } 68 | } 69 | } 70 | 71 | impl ToString for Difficulty { 72 | fn to_string(&self) -> String { 73 | let level: DifficultyType = self.into(); 74 | match level { 75 | Easy => Green.paint(Easy.to_string()).to_string(), 76 | Medium => Yellow.paint(Medium.to_string()).to_string(), 77 | Hard => Red.paint(Hard.to_string()).to_string(), 78 | } 79 | } 80 | } 81 | 82 | pub type ProblemInfoSeq = Vec>; 83 | 84 | pub trait ProblemInfo { 85 | fn question_id(&self) -> usize; 86 | fn question_title(&self) -> &str; 87 | fn difficulty(&self) -> &Difficulty; 88 | fn is_favorite(&self) -> Option; 89 | fn is_paid_only(&self) -> bool; 90 | fn status(&self) -> Option<&str>; 91 | } 92 | 93 | impl PartialEq for dyn ProblemInfo + '_ + Send { 94 | fn eq(&self, other: &Self) -> bool { 95 | self.question_id().eq(&other.question_id()) 96 | } 97 | } 98 | 99 | impl Eq for dyn ProblemInfo + '_ + Send {} 100 | 101 | impl PartialOrd for dyn ProblemInfo + '_ + Send { 102 | fn partial_cmp(&self, other: &Self) -> Option { 103 | Some(self.cmp(other)) 104 | } 105 | } 106 | 107 | impl Ord for dyn ProblemInfo + '_ + Send { 108 | fn cmp(&self, other: &Self) -> Ordering { 109 | self.question_id().cmp(&other.question_id()) 110 | } 111 | } 112 | 113 | impl PartialEq for dyn ProblemInfo { 114 | fn eq(&self, other: &Self) -> bool { 115 | self.question_id().eq(&other.question_id()) 116 | } 117 | } 118 | 119 | impl Eq for dyn ProblemInfo {} 120 | 121 | impl PartialOrd for dyn ProblemInfo { 122 | fn partial_cmp(&self, other: &Self) -> Option { 123 | Some(self.cmp(other)) 124 | } 125 | } 126 | 127 | impl Ord for dyn ProblemInfo { 128 | fn cmp(&self, other: &Self) -> Ordering { 129 | self.question_id().cmp(&other.question_id()) 130 | } 131 | } 132 | 133 | #[derive(Deserialize, Debug)] 134 | pub struct Stat { 135 | pub question_id: usize, 136 | 137 | #[serde(rename = "question__article__live")] 138 | pub question_article_live: Option, 139 | 140 | #[serde(rename = "question__article__slug")] 141 | pub question_article_slug: Option, 142 | 143 | #[serde(rename = "question__title")] 144 | pub question_title: String, 145 | 146 | #[serde(rename = "question__title_slug")] 147 | pub question_title_slug: String, 148 | 149 | #[serde(rename = "question__hide")] 150 | pub question_hide: bool, 151 | 152 | pub total_acs: usize, 153 | pub total_submitted: usize, 154 | pub frontend_question_id: usize, 155 | pub is_new_question: bool, 156 | } 157 | 158 | #[derive(Deserialize, Debug)] 159 | pub struct StatStatusPair { 160 | pub stat: Stat, 161 | pub status: Option, 162 | pub difficulty: Difficulty, 163 | pub paid_only: bool, 164 | pub is_favor: bool, 165 | pub frequency: f64, 166 | pub progress: f64, 167 | } 168 | 169 | #[derive(Deserialize, Debug)] 170 | pub struct TopicTagQuestion { 171 | pub status: Option, 172 | pub difficulty: Difficulty, 173 | pub title: String, 174 | 175 | #[serde(rename = "isPaidOnly")] 176 | pub is_paid_only: bool, 177 | 178 | #[serde(rename = "titleSlug")] 179 | pub title_slug: String, 180 | 181 | #[serde(rename = "questionFrontendId")] 182 | pub question_frontend_id: String, 183 | } 184 | 185 | #[derive(Deserialize, Debug)] 186 | pub struct ListResponse { 187 | pub user_name: String, 188 | pub num_solved: usize, 189 | pub num_total: usize, 190 | pub ac_easy: usize, 191 | pub ac_medium: usize, 192 | pub ac_hard: usize, 193 | pub stat_status_pairs: Vec, 194 | pub frequency_high: usize, 195 | pub frequency_mid: usize, 196 | pub category_slug: String, 197 | } 198 | 199 | #[derive(Deserialize, Debug)] 200 | pub struct CodeDefinition { 201 | pub value: String, 202 | pub text: String, 203 | 204 | #[serde(rename = "defaultCode")] 205 | pub default_code: String, 206 | } 207 | 208 | #[derive(Deserialize, Debug)] 209 | pub struct SubmissionResponse { 210 | pub state: Option, 211 | pub input: Option, 212 | pub input_formatted: Option, 213 | pub code_output: Option, 214 | pub std_output: Option, 215 | pub last_test_case: Option, 216 | pub correct_answer: Option, 217 | pub code_answer: Option, 218 | pub expected_output: Option, 219 | pub expected_code_output: Option, 220 | pub expected_answer: Option, 221 | pub expected_code_answer: Option, 222 | pub compare_result: Option, 223 | pub compile_error: Option, 224 | pub full_compile_error: Option, 225 | pub lang: String, 226 | pub memory: Option, 227 | pub memory_percentile: Option, 228 | pub pretty_lang: String, 229 | pub run_success: bool, 230 | pub runtime_percentile: Option, 231 | pub expected_status_code: Option, 232 | pub status_memory: String, 233 | pub status_msg: String, 234 | pub status_runtime: String, 235 | pub submission_id: String, 236 | pub total_correct: Option, 237 | pub total_testcases: Option, 238 | } 239 | 240 | pub trait ExecutionErrorResponse { 241 | fn has_compile_error(&self) -> bool; 242 | 243 | fn has_runtime_error(&self) -> bool; 244 | 245 | fn has_error(&self) -> bool; 246 | 247 | fn is_error(&self) -> bool { 248 | self.has_compile_error() || self.has_runtime_error() || self.has_error() 249 | } 250 | } 251 | 252 | impl ExecutionErrorResponse for SubmissionResponse { 253 | fn has_compile_error(&self) -> bool { 254 | self.compile_error.is_some() || self.full_compile_error.is_some() 255 | } 256 | 257 | fn has_runtime_error(&self) -> bool { 258 | let tle = "time limit exceeded"; 259 | let msg = self.status_msg.to_lowercase(); 260 | 261 | msg.eq(tle) || msg.contains("error") 262 | } 263 | 264 | fn has_error(&self) -> bool { 265 | self.total_correct.lt(&self.total_testcases) 266 | } 267 | } 268 | 269 | impl ProblemInfo for StatStatusPair { 270 | fn question_id(&self) -> usize { 271 | self.stat.frontend_question_id 272 | } 273 | 274 | fn question_title(&self) -> &str { 275 | self.stat.question_title.as_str() 276 | } 277 | 278 | fn difficulty(&self) -> &Difficulty { 279 | &self.difficulty 280 | } 281 | 282 | fn is_favorite(&self) -> Option { 283 | Some(self.is_favor) 284 | } 285 | 286 | fn is_paid_only(&self) -> bool { 287 | self.paid_only 288 | } 289 | 290 | fn status(&self) -> Option<&str> { 291 | self.status.as_ref().map(String::as_ref) 292 | } 293 | } 294 | 295 | impl ProblemInfo for TopicTagQuestion { 296 | fn question_id(&self) -> usize { 297 | self.question_frontend_id 298 | .parse() 299 | .expect("Expected question_frontend_id") 300 | } 301 | 302 | fn question_title(&self) -> &str { 303 | self.title.as_str() 304 | } 305 | 306 | fn difficulty(&self) -> &Difficulty { 307 | &self.difficulty 308 | } 309 | 310 | fn is_favorite(&self) -> Option { 311 | None 312 | } 313 | 314 | fn is_paid_only(&self) -> bool { 315 | self.is_paid_only 316 | } 317 | 318 | fn status(&self) -> Option<&str> { 319 | self.status.as_ref().map(String::as_ref) 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /src/printer/mod.rs: -------------------------------------------------------------------------------- 1 | mod printer; 2 | mod submit_execution_printer; 3 | mod test_execution_printer; 4 | 5 | pub use printer::*; 6 | pub use submit_execution_printer::SubmitExecutionResult; 7 | pub use test_execution_printer::TestExecutionResult; 8 | -------------------------------------------------------------------------------- /src/printer/printer.rs: -------------------------------------------------------------------------------- 1 | use crate::model::SubmissionResponse; 2 | 3 | pub(crate) const NEW_LINE: &'static str = "\n"; 4 | pub(crate) const TEXT_BOLD_ON: &'static str = "\x1b[1m"; 5 | pub(crate) const TEXT_BOLD_OFF: &'static str = "\x1b[m"; 6 | 7 | pub trait Printer { 8 | fn print(&self) { 9 | print!("{}", self.buffer()); 10 | } 11 | 12 | fn is_error(&self) -> bool; 13 | 14 | fn buffer(&self) -> String; 15 | 16 | fn total_cases_ratio_buffer(&self, response: &SubmissionResponse) -> String { 17 | format!( 18 | "{}/{}", 19 | response.total_correct.unwrap_or(0), 20 | response.total_testcases.unwrap_or(0) 21 | ) 22 | } 23 | } 24 | 25 | pub mod decorator { 26 | use super::*; 27 | 28 | pub fn bold_text(s: &str) -> String { 29 | format!("{}{}{}", s, TEXT_BOLD_ON, TEXT_BOLD_OFF) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/printer/submit_execution_printer.rs: -------------------------------------------------------------------------------- 1 | use colci::Color; 2 | 3 | use crate::model::ExecutionErrorResponse; 4 | use crate::printer::{decorator::bold_text, Printer, NEW_LINE}; 5 | use crate::{icon::Icon, model::SubmissionResponse, Either}; 6 | 7 | #[derive(Debug)] 8 | pub struct SubmitExecutionResult { 9 | submission_response: SubmissionResponse, 10 | } 11 | 12 | impl Printer for SubmitExecutionResult { 13 | fn is_error(&self) -> bool { 14 | self.submission_response.is_error() 15 | } 16 | 17 | fn buffer(&self) -> String { 18 | if self.is_error() { 19 | self.error_buffer() 20 | } else { 21 | self.success_buffer() 22 | } 23 | } 24 | } 25 | 26 | impl SubmitExecutionResult { 27 | pub fn new(submission_response: SubmissionResponse) -> Self { 28 | Self { 29 | submission_response, 30 | } 31 | } 32 | 33 | fn error_buffer(&self) -> String { 34 | let error_buffer = self.runtime_error_buffer() 35 | + NEW_LINE 36 | + NEW_LINE 37 | + self.compile_error_buffer().as_str(); 38 | if error_buffer.trim().is_empty() { 39 | self.wrong_answer_buffer() 40 | } else { 41 | error_buffer 42 | } 43 | } 44 | 45 | fn runtime_error_buffer(&self) -> String { 46 | if !self.submission_response.has_runtime_error() { 47 | return NEW_LINE.to_owned(); 48 | } 49 | self.submission_response.status_msg.to_owned() 50 | } 51 | 52 | fn compile_error_buffer(&self) -> String { 53 | if !self.submission_response.has_compile_error() { 54 | return NEW_LINE.to_owned(); 55 | } 56 | self.submission_response 57 | .full_compile_error 58 | .to_owned() 59 | .unwrap_or_default() 60 | } 61 | 62 | fn wrong_answer_buffer(&self) -> String { 63 | let mut buffer = String::new(); 64 | buffer.push_str(&bold_text( 65 | &Color::Red(&format!( 66 | "\n{} Wrong Answer: ({})\n\n", 67 | Icon::_No.to_string(), 68 | self.total_cases_ratio_buffer(&self.submission_response) 69 | )) 70 | .make(), 71 | )); 72 | buffer.push_str(&self.last_test_case_buffer()); 73 | buffer.push_str(&Color::Red(&self.get_metas()).make()); 74 | 75 | buffer 76 | } 77 | 78 | fn last_test_case_buffer(&self) -> String { 79 | let mut buffer = String::new(); 80 | match ( 81 | &self.submission_response.input, 82 | &self.submission_response.code_output, 83 | &self.submission_response.expected_output, 84 | ) { 85 | ( 86 | Some(Either::String(input)), 87 | Some(Either::String(ans)), 88 | Some(Either::String(exp_ans)), 89 | ) => { 90 | let mut test_case = String::new(); 91 | test_case.push_str(&Color::Red("Last test case:\n").make()); 92 | test_case.push_str(&format!( 93 | "\tInput: \n\t\t{}\n", 94 | input.replace('\n', "\n\t\t") 95 | )); 96 | test_case.push_str(&format!("\n\tOutput: {}\n", ans)); 97 | test_case.push_str(&format!("\tExpected: {}\n\n", exp_ans)); 98 | 99 | buffer.push_str(test_case.as_str()); 100 | } 101 | _ => {} 102 | } 103 | 104 | buffer 105 | } 106 | 107 | fn success_buffer(&self) -> String { 108 | let mut buffer = String::new(); 109 | buffer.push_str(&bold_text( 110 | &Color::Green(&format!( 111 | "{} Accepted: ({})\n\n", 112 | Icon::Yes.to_string(), 113 | self.total_cases_ratio_buffer(&self.submission_response) 114 | )) 115 | .make(), 116 | )); 117 | buffer.push_str(&self.last_test_case_buffer()); 118 | buffer.push_str(&Color::Green(&self.get_metas()).make()); 119 | 120 | buffer 121 | } 122 | 123 | fn get_metas(&self) -> String { 124 | if self.is_error() { 125 | return NEW_LINE.to_string(); 126 | } 127 | let memory_percentile = self 128 | .submission_response 129 | .memory_percentile 130 | .unwrap_or(0.0) 131 | .to_string(); 132 | let runtime_percentile = self 133 | .submission_response 134 | .runtime_percentile 135 | .unwrap_or(0.0) 136 | .to_string(); 137 | let metas = vec![ 138 | "Memory: ".to_string() + self.submission_response.status_memory.as_str(), 139 | "Memory %ile: ".to_string() + memory_percentile.as_str(), 140 | "Runtime: ".to_string() + self.submission_response.status_runtime.as_str(), 141 | "Runtime %ile: ".to_string() + runtime_percentile.as_str(), 142 | "\n\n".to_string(), 143 | ]; 144 | 145 | metas.join("\n") 146 | } 147 | } 148 | 149 | #[cfg(test)] 150 | mod tests { 151 | use super::{Printer, SubmitExecutionResult}; 152 | use crate::model::SubmissionResponse; 153 | use serde_json::from_value; 154 | 155 | #[test] 156 | fn print_submit_wrong_answer() { 157 | let json_value = serde_json::from_str( 158 | r#"{ 159 | "status_code": 11, 160 | "lang": "python3", 161 | "run_success": true, 162 | "status_runtime": "N/A", 163 | "memory": 16468000, 164 | "question_id": "10", 165 | "elapsed_time": 65, 166 | "compare_result": "1110011111111111100011110100100011010111111111111111111101110111111111111111111111110111111110100111010111111111111111011101111111110011111111111111111110101101110010111011011101111101111111110111101011101111101111111111111101101110110111101110011111101001111110110110101101110011001111111111111111001110000010110111111111111110111110111110011111001001010", 167 | "code_output": "false", 168 | "std_output": "", 169 | "last_testcase": "\"aab\"\n\"c*a*b\"", 170 | "expected_output": "true", 171 | "task_finish_time": 1694277425292, 172 | "task_name": "judger.judgetask.Judge", 173 | "finished": true, 174 | "total_correct": 277, 175 | "total_testcases": 355, 176 | "runtime_percentile": null, 177 | "status_memory": "N/A", 178 | "memory_percentile": null, 179 | "pretty_lang": "Python3", 180 | "submission_id": "1044868319", 181 | "input_formatted": "\"aab\", \"c*a*b\"", 182 | "input": "\"aab\"\n\"c*a*b\"", 183 | "status_msg": "Wrong Answer", 184 | "state": "SUCCESS" 185 | }"# 186 | ) 187 | .unwrap(); 188 | 189 | let response = from_value::(json_value).unwrap().into(); 190 | 191 | let result = SubmitExecutionResult::new(response); 192 | result.print(); 193 | // TODO implement snapshot testing 194 | assert!(1 == 1); 195 | } 196 | 197 | #[test] 198 | fn print_submit_accepted() { 199 | let json_value = serde_json::from_str( 200 | r#"{ 201 | "status_code": 10, 202 | "lang": "rust", 203 | "run_success": true, 204 | "status_runtime": "0 ms", 205 | "memory": 1928000, 206 | "question_id": "10", 207 | "elapsed_time": 10, 208 | "compare_result": "1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111", 209 | "code_output": "", 210 | "std_output": "", 211 | "last_testcase": "", 212 | "expected_output": "", 213 | "task_finish_time": 1694280355711, 214 | "task_name": "judger.judgetask.Judge", 215 | "finished": true, 216 | "total_correct": 355, 217 | "total_testcases": 355, 218 | "runtime_percentile": 100, 219 | "status_memory": "1.9 MB", 220 | "memory_percentile": 91.83670000000001, 221 | "pretty_lang": "Rust", 222 | "submission_id": "1044907055", 223 | "status_msg": "Accepted", 224 | "state": "SUCCESS" 225 | }"# 226 | ) 227 | .unwrap(); 228 | 229 | let response = from_value::(json_value).unwrap().into(); 230 | 231 | let result = SubmitExecutionResult::new(response); 232 | result.print(); 233 | // TODO implement snapshot testing 234 | assert!(1 == 1); 235 | } 236 | 237 | #[test] 238 | fn print_submit_wrong_answer_seq() { 239 | let json_value = serde_json::from_str( 240 | r#"{ 241 | "status_code": 11, 242 | "lang": "java", 243 | "run_success": true, 244 | "status_runtime": "N/A", 245 | "memory": 44740000, 246 | "display_runtime": "261", 247 | "question_id": "15", 248 | "elapsed_time": 467, 249 | "compare_result": "111111111111111111110010011100110000111100001101000100000010111101100110011101001001000000010101101000000000000001111100001000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100", 250 | "code_output": "[[-2,-1,3],[-2,0,2]]", 251 | "std_output": "", 252 | "last_testcase": "[3,0,-2,-1,1,2]", 253 | "expected_output": "[[-2,-1,3],[-2,0,2],[-1,0,1]]", 254 | "task_finish_time": 1694280912080, 255 | "task_name": "judger.judgetask.Judge", 256 | "finished": true, 257 | "total_correct": 63, 258 | "total_testcases": 312, 259 | "runtime_percentile": null, 260 | "status_memory": "N/A", 261 | "memory_percentile": null, 262 | "pretty_lang": "Java", 263 | "submission_id": "1044914848", 264 | "input_formatted": "[3,0,-2,-1,1,2]", 265 | "input": "[3,0,-2,-1,1,2]", 266 | "status_msg": "Wrong Answer", 267 | "state": "SUCCESS" 268 | }"# 269 | ) 270 | .unwrap(); 271 | 272 | let response = from_value::(json_value).unwrap().into(); 273 | 274 | let result = SubmitExecutionResult::new(response); 275 | result.print(); 276 | // TODO implement snapshot testing 277 | assert!(1 == 1); 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /src/printer/test_execution_printer.rs: -------------------------------------------------------------------------------- 1 | use colci::Color; 2 | 3 | use crate::model::ExecutionErrorResponse; 4 | use crate::printer::{decorator::bold_text, Printer, NEW_LINE}; 5 | use crate::{icon::Icon, model::SubmissionResponse, Either}; 6 | 7 | #[derive(Debug)] 8 | pub struct TestExecutionResult { 9 | test_data: Either, 10 | submission_response: SubmissionResponse, 11 | } 12 | 13 | impl Printer for TestExecutionResult { 14 | fn is_error(&self) -> bool { 15 | self.submission_response.is_error() 16 | } 17 | 18 | fn buffer(&self) -> String { 19 | if self.is_error() { 20 | self.error_buffer() 21 | } else { 22 | self.success_buffer() 23 | } 24 | } 25 | } 26 | 27 | impl TestExecutionResult { 28 | pub fn new(test_data: Either, submission_result: SubmissionResponse) -> Self { 29 | Self { 30 | test_data, 31 | submission_response: submission_result, 32 | } 33 | } 34 | 35 | fn error_buffer(&self) -> String { 36 | let error_buffer = self.runtime_error_buffer() 37 | + NEW_LINE 38 | + NEW_LINE 39 | + self.compile_error_buffer().as_str(); 40 | if error_buffer.trim().is_empty() { 41 | self.wrong_answer_buffer() 42 | } else { 43 | error_buffer 44 | } 45 | } 46 | 47 | fn runtime_error_buffer(&self) -> String { 48 | if !self.submission_response.has_runtime_error() { 49 | return NEW_LINE.to_owned(); 50 | } 51 | self.submission_response.status_msg.to_owned() 52 | } 53 | 54 | fn compile_error_buffer(&self) -> String { 55 | if !self.submission_response.has_compile_error() { 56 | return NEW_LINE.to_owned(); 57 | } 58 | self.submission_response 59 | .full_compile_error 60 | .to_owned() 61 | .unwrap_or_default() 62 | } 63 | 64 | fn wrong_answer_buffer(&self) -> String { 65 | let mut buffer = String::new(); 66 | buffer.push_str(&bold_text( 67 | &Color::Red(&format!( 68 | "\n{} Wrong Answer: ({})\n\n", 69 | Icon::_No.to_string(), 70 | self.total_cases_ratio_buffer(&self.submission_response) 71 | )) 72 | .make(), 73 | )); 74 | buffer.push_str(&self.test_cases_buffer()); 75 | buffer.push_str(&Color::Red(&self.get_metas()).make()); 76 | 77 | buffer 78 | } 79 | 80 | fn test_cases_buffer(&self) -> String { 81 | let mut buffer = String::new(); 82 | // combine test_data, code_answer & expected_code_answer 83 | match ( 84 | &self.test_data, 85 | &self.submission_response.code_answer, 86 | &self.submission_response.expected_code_answer, 87 | ) { 88 | ( 89 | Either::Sequence(input_seq), 90 | Some(Either::Sequence(ans_seq)), 91 | Some(Either::Sequence(exp_ans_seq)), 92 | ) => { 93 | let chunk_size = input_seq.len() / ans_seq.len(); 94 | let input_chunks: Vec> = input_seq 95 | .chunks(chunk_size) 96 | .map(|chunk| chunk.to_vec()) 97 | .collect(); 98 | for (i, ((input, ans), exp_ans)) in input_chunks 99 | .iter() 100 | .zip(ans_seq) 101 | .zip(exp_ans_seq) 102 | .enumerate() 103 | { 104 | let mut test_case = String::new(); 105 | let is_correct = ans.eq(exp_ans); 106 | let colored_case = if is_correct { 107 | Color::Green(&format!("{} Case {}:\n", Icon::Yes.to_string(), i + 1)).make() 108 | } else { 109 | Color::Red(&format!("{} Case {}:\n", Icon::_No.to_string(), i + 1)).make() 110 | }; 111 | test_case.push_str(&colored_case); 112 | test_case.push_str(&format!("\tInput: \n\t\t{}\n", input.join("\n\t\t"))); 113 | test_case.push_str(&format!("\n\tOutput: {}\n", ans)); 114 | test_case.push_str(&format!("\tExpected: {}\n\n", exp_ans)); 115 | 116 | buffer.push_str(test_case.as_str()); 117 | } 118 | } 119 | _ => {} 120 | } 121 | 122 | buffer 123 | } 124 | 125 | fn success_buffer(&self) -> String { 126 | let mut buffer = String::new(); 127 | buffer.push_str(&bold_text( 128 | &Color::Green(&format!( 129 | "{} Accepted: ({})\n\n", 130 | Icon::Yes.to_string(), 131 | self.total_cases_ratio_buffer(&self.submission_response) 132 | )) 133 | .make(), 134 | )); 135 | buffer.push_str(&self.test_cases_buffer()); 136 | buffer.push_str(&Color::Green(&self.get_metas()).make()); 137 | 138 | buffer 139 | } 140 | 141 | fn get_metas(&self) -> String { 142 | let memory_percentile = self 143 | .submission_response 144 | .memory_percentile 145 | .unwrap_or(0.0) 146 | .to_string(); 147 | let runtime_percentile = self 148 | .submission_response 149 | .runtime_percentile 150 | .unwrap_or(0.0) 151 | .to_string(); 152 | let testcases = format!( 153 | "{}/{}", 154 | self.submission_response.total_correct.unwrap_or(0), 155 | self.submission_response.total_testcases.unwrap_or(0) 156 | ); 157 | let accepted_meta = format!( 158 | "{}: ({})", 159 | self.submission_response.status_msg.as_str(), 160 | testcases.as_str() 161 | ); 162 | let metas = vec![ 163 | accepted_meta, 164 | "Memory: ".to_string() + self.submission_response.status_memory.as_str(), 165 | "Memory %ile: ".to_string() + memory_percentile.as_str(), 166 | "Runtime: ".to_string() + self.submission_response.status_runtime.as_str(), 167 | "Runtime %ile: ".to_string() + runtime_percentile.as_str(), 168 | "\n\n".to_string(), 169 | ]; 170 | 171 | metas.join("\n") 172 | } 173 | } 174 | 175 | #[cfg(test)] 176 | mod tests { 177 | use super::{Printer, TestExecutionResult}; 178 | use crate::{model::SubmissionResponse, Either}; 179 | use serde_json::from_value; 180 | 181 | #[test] 182 | fn print_sequence_success() { 183 | let test_data = Either::Sequence(vec![ 184 | "[1,2,3]".to_owned(), 185 | "[1,0,-1,-1,-1,1,0,1]".to_owned(), 186 | "[1,0,-1]".to_owned(), 187 | "[-1,0,1,2,-1,-4]".to_owned(), 188 | "[0,1,1]".to_owned(), 189 | "[0,0,0]".to_owned(), 190 | ]); 191 | let json_value = serde_json::from_str( 192 | r#"{ 193 | "status_code": 10, 194 | "lang": "java", 195 | "run_success": true, 196 | "status_runtime": "3 ms", 197 | "memory": 40404000, 198 | "display_runtime": "3", 199 | "code_answer": [ 200 | "[]", 201 | "[[-1,0,1]]", 202 | "[[-1,0,1]]", 203 | "[[-1,-1,2],[-1,0,1]]", 204 | "[]", 205 | "[[0,0,0]]" 206 | ], 207 | "code_output": [], 208 | "std_output_list": [ 209 | "", 210 | "", 211 | "", 212 | "", 213 | "", 214 | "", 215 | "" 216 | ], 217 | "elapsed_time": 217, 218 | "task_finish_time": 1693886584999, 219 | "task_name": "judger.runcodetask.RunCode", 220 | "expected_status_code": 10, 221 | "expected_lang": "cpp", 222 | "expected_run_success": true, 223 | "expected_status_runtime": "0", 224 | "expected_memory": 6304000, 225 | "expected_code_answer": [ 226 | "[]", 227 | "[[-1,0,1]]", 228 | "[[-1,0,1]]", 229 | "[[-1,-1,2],[-1,0,1]]", 230 | "[]", 231 | "[[0,0,0]]" 232 | ], 233 | "expected_code_output": [], 234 | "expected_std_output_list": [ 235 | "", 236 | "", 237 | "", 238 | "", 239 | "", 240 | "", 241 | "" 242 | ], 243 | "expected_elapsed_time": 21, 244 | "expected_task_finish_time": 1693885714905, 245 | "expected_task_name": "judger.interprettask.Interpret", 246 | "correct_answer": true, 247 | "compare_result": "111111", 248 | "total_correct": 6, 249 | "total_testcases": 6, 250 | "runtime_percentile": null, 251 | "status_memory": "40.4 MB", 252 | "memory_percentile": null, 253 | "pretty_lang": "Java", 254 | "submission_id": "runcode_1693886582.3348386_GUkqbCdnmN", 255 | "status_msg": "Accepted", 256 | "state": "SUCCESS" 257 | }"#, 258 | ) 259 | .unwrap(); 260 | 261 | let response = from_value::(json_value).unwrap().into(); 262 | 263 | let result = TestExecutionResult::new(test_data, response); 264 | result.print(); 265 | // TODO implement snapshot testing 266 | assert!(1 == 1); 267 | } 268 | 269 | #[test] 270 | fn print_sequence_wrong_answer() { 271 | let test_data = Either::Sequence(vec![ 272 | "[1,2,3]".to_owned(), 273 | "[1,0,-1,-1,-1,1,0,1]".to_owned(), 274 | "[1,0,-1]".to_owned(), 275 | "[-1,0,1,2,-1,-4]".to_owned(), 276 | "[0,1,1]".to_owned(), 277 | "[0,0,0]".to_owned(), 278 | ]); 279 | let json_value = serde_json::from_str( 280 | r#"{ 281 | "status_code": 10, 282 | "lang": "java", 283 | "run_success": true, 284 | "status_runtime": "2 ms", 285 | "memory": 40560000, 286 | "display_runtime": "2", 287 | "code_answer": [ 288 | "[]", 289 | "[[-1,0,1]]", 290 | "[[-1,0,1]]", 291 | "[[-1,-1,2]]", 292 | "[]", 293 | "[[0,0,0]]" 294 | ], 295 | "code_output": [], 296 | "std_output_list": [ 297 | "", 298 | "", 299 | "", 300 | "", 301 | "", 302 | "", 303 | "" 304 | ], 305 | "elapsed_time": 229, 306 | "task_finish_time": 1693885714946, 307 | "task_name": "judger.runcodetask.RunCode", 308 | "expected_status_code": 10, 309 | "expected_lang": "cpp", 310 | "expected_run_success": true, 311 | "expected_status_runtime": "0", 312 | "expected_memory": 6304000, 313 | "expected_code_answer": [ 314 | "[]", 315 | "[[-1,0,1]]", 316 | "[[-1,0,1]]", 317 | "[[-1,-1,2],[-1,0,1]]", 318 | "[]", 319 | "[[0,0,0]]" 320 | ], 321 | "expected_code_output": [], 322 | "expected_std_output_list": [ 323 | "", 324 | "", 325 | "", 326 | "", 327 | "", 328 | "", 329 | "" 330 | ], 331 | "expected_elapsed_time": 21, 332 | "expected_task_finish_time": 1693885714905, 333 | "expected_task_name": "judger.interprettask.Interpret", 334 | "correct_answer": false, 335 | "compare_result": "111011", 336 | "total_correct": 5, 337 | "total_testcases": 6, 338 | "runtime_percentile": null, 339 | "status_memory": "40.6 MB", 340 | "memory_percentile": null, 341 | "pretty_lang": "Java", 342 | "submission_id": "runcode_1693885710.9158015_9uDhRiWjFV", 343 | "status_msg": "Accepted", 344 | "state": "SUCCESS" 345 | }"#, 346 | ) 347 | .unwrap(); 348 | 349 | let response = from_value::(json_value).unwrap().into(); 350 | 351 | let result = TestExecutionResult::new(test_data, response); 352 | result.print(); 353 | // TODO implement snapshot testing 354 | assert!(1 == 1); 355 | } 356 | 357 | #[test] 358 | fn print_multi_input_accepted() { 359 | let test_data = Either::Sequence(vec![ 360 | "aa".to_owned(), 361 | "a".to_owned(), 362 | "aa".to_owned(), 363 | "a*".to_owned(), 364 | "ab".to_owned(), 365 | ".*".to_owned(), 366 | ]); 367 | let json_value = serde_json::from_str( 368 | r#"{"status_code": 10, "lang": "rust", "run_success": true, "status_runtime": "0 ms", "memory": 2096000, "code_answer": ["false", "true", "true"], "code_output": [], "std_output_list": ["", "", "", ""], "elapsed_time": 39, "task_finish_time": 1694281117287, "task_name": "judger.runcodetask.RunCode", "expected_status_code": 10, "expected_lang": "cpp", "expected_run_success": true, "expected_status_runtime": "0", "expected_memory": 5936000, "expected_code_answer": ["false", "true", "true"], "expected_code_output": [], "expected_std_output_list": ["", "", "", ""], "expected_elapsed_time": 37, "expected_task_finish_time": 1694281041897, "expected_task_name": "judger.interprettask.Interpret", "correct_answer": true, "compare_result": "111", "total_correct": 3, "total_testcases": 3, "runtime_percentile": null, "status_memory": "2.1 MB", "memory_percentile": null, "pretty_lang": "Rust", "submission_id": "runcode_1694281112.034828_DfKA6OnxO1", "status_msg": "Accepted", "state": "SUCCESS"}"# 369 | ) 370 | .unwrap(); 371 | 372 | let response = from_value::(json_value).unwrap().into(); 373 | 374 | let result = TestExecutionResult::new(test_data, response); 375 | result.print(); 376 | // TODO implement snapshot testing 377 | assert!(1 == 1); 378 | } 379 | 380 | #[test] 381 | fn print_multi_input_wrong_answer() { 382 | let test_data = Either::Sequence(vec![ 383 | "aa".to_owned(), 384 | "a".to_owned(), 385 | "aa".to_owned(), 386 | "a*".to_owned(), 387 | "ab".to_owned(), 388 | ".*".to_owned(), 389 | ]); 390 | let json_value = serde_json::from_str( 391 | r#"{"status_code": 10, "lang": "rust", "run_success": true, "status_runtime": "1 ms", "memory": 2000000, "code_answer": ["false", "false", "false"], "code_output": [], "std_output_list": ["", "", "", ""], "elapsed_time": 16, "task_finish_time": 1694281884439, "task_name": "judger.runcodetask.RunCode", "expected_status_code": 10, "expected_lang": "cpp", "expected_run_success": true, "expected_status_runtime": "0", "expected_memory": 5936000, "expected_code_answer": ["false", "true", "true"], "expected_code_output": [], "expected_std_output_list": ["", "", "", ""], "expected_elapsed_time": 37, "expected_task_finish_time": 1694281041897, "expected_task_name": "judger.interprettask.Interpret", "correct_answer": false, "compare_result": "100", "total_correct": 1, "total_testcases": 3, "runtime_percentile": null, "status_memory": "2 MB", "memory_percentile": null, "pretty_lang": "Rust", "submission_id": "runcode_1694281880.8477898_KECPBvM8Vm", "status_msg": "Accepted", "state": "SUCCESS"}"# 392 | ) 393 | .unwrap(); 394 | 395 | let response = from_value::(json_value).unwrap().into(); 396 | 397 | let result = TestExecutionResult::new(test_data, response); 398 | result.print(); 399 | // TODO implement snapshot testing 400 | assert!(1 == 1); 401 | } 402 | } 403 | -------------------------------------------------------------------------------- /src/service/auth.rs: -------------------------------------------------------------------------------- 1 | use std::io::{BufWriter, Write}; 2 | 3 | use colci::Color; 4 | 5 | use crate::{ 6 | service::{ServiceProvider, Session}, 7 | Result, 8 | }; 9 | 10 | pub async fn cookie_login<'a, P: ServiceProvider<'a>>(_provider: &P) -> Result { 11 | let mut out = BufWriter::new(std::io::stdout()); 12 | let stdin = std::io::stdin(); 13 | 14 | let mut csrf = String::new(); 15 | let mut lc_session = String::new(); 16 | 17 | write!(out, "{}", Color::Yellow("csrftoken: ").make())?; 18 | out.flush()?; 19 | stdin.read_line(&mut csrf)?; 20 | 21 | write!(out, "{}", Color::Yellow("LEETCODE_SESSION: ").make())?; 22 | out.flush()?; 23 | stdin.read_line(&mut lc_session)?; 24 | 25 | csrf = csrf.trim().to_string(); 26 | lc_session = lc_session.trim().to_string(); 27 | 28 | println!("{}", Color::Green("User logged in!").make()); 29 | 30 | Ok(Session::new(lc_session.to_string(), csrf.to_string())) 31 | } 32 | -------------------------------------------------------------------------------- /src/service/file.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fs::File; 3 | use std::io::Read; 4 | use std::path::Path; 5 | use std::str::FromStr; 6 | 7 | use log::*; 8 | 9 | use crate::model::Problem; 10 | use crate::{template::Pattern, LeetUpError, Result}; 11 | 12 | impl FromStr for Problem { 13 | type Err = LeetUpError; 14 | 15 | fn from_str(s: &str) -> std::result::Result { 16 | info!("LeetupInfo: {}", s); 17 | let map: HashMap<_, _> = s 18 | .split(' ') 19 | .map(|e| { 20 | let split = e.split('=').collect::>(); 21 | (split[0], split[1]) 22 | }) 23 | .collect(); 24 | let id: usize = map.get("id").unwrap().parse().unwrap(); 25 | let slug = map.get("slug").unwrap().to_string(); 26 | let lang = map.get("lang").unwrap().to_string(); 27 | let link = format!("https://leetcode.com/problems/{}/submissions/", slug); 28 | Ok(Self { 29 | id, 30 | slug, 31 | lang, 32 | link, 33 | typed_code: None, 34 | }) 35 | } 36 | } 37 | 38 | pub fn extract_problem>(filename: P) -> Result { 39 | debug!("Filename: {:#?}", filename.as_ref()); 40 | let mut typed_code = String::new(); 41 | let mut file = File::open(filename)?; 42 | file.read_to_string(&mut typed_code)?; 43 | let pattern_leetup_info: String = Pattern::LeetUpInfo.into(); 44 | let info_index = typed_code 45 | .find(&pattern_leetup_info) 46 | .map(|i| i + pattern_leetup_info.len()) 47 | .expect("LeetUpInfo is required."); 48 | let line = &typed_code[info_index..].trim(); 49 | let end_index = line.find('\n').expect("LeetupInfo needs a new line"); 50 | let line = &line[..end_index].trim(); 51 | let mut problem = Problem::from_str(line)?; 52 | problem.typed_code = Some(typed_code); 53 | debug!("{:#?}", problem); 54 | 55 | Ok(problem) 56 | } 57 | -------------------------------------------------------------------------------- /src/service/lang.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use anyhow::anyhow; 4 | use serde::{de, Deserialize}; 5 | 6 | use crate::LeetUpError; 7 | 8 | /// Store Lang attributes. 9 | #[derive(Debug, Clone, Deserialize)] 10 | pub struct LangInfo { 11 | pub name: String, 12 | pub extension: String, 13 | pub comment: Comment, 14 | } 15 | 16 | #[derive(Debug, Clone, Deserialize)] 17 | pub enum CommentStyle { 18 | Single(String), 19 | Multiline { 20 | start: String, 21 | between: String, 22 | end: String, 23 | }, 24 | } 25 | 26 | /// Comment for different languages. 27 | #[derive(Debug, Clone, Deserialize)] 28 | pub enum Comment { 29 | C(CommentStyle, Option), 30 | Python3(CommentStyle, Option), 31 | MySQL(CommentStyle, Option), 32 | } 33 | 34 | /// Represent different languages supported by a Service provider. 35 | #[derive(Debug, Clone)] 36 | pub enum Lang { 37 | Rust(LangInfo), 38 | Java(LangInfo), 39 | Javascript(LangInfo), 40 | Python3(LangInfo), 41 | MySQL(LangInfo), 42 | Cpp(LangInfo), 43 | Ruby(LangInfo), 44 | C(LangInfo), 45 | CSharp(LangInfo), 46 | Go(LangInfo), 47 | Php(LangInfo), 48 | Kotlin(LangInfo), 49 | Scala(LangInfo), 50 | Swift(LangInfo), 51 | Typescript(LangInfo), 52 | } 53 | 54 | impl FromStr for Lang { 55 | type Err = LeetUpError; 56 | 57 | fn from_str(s: &str) -> Result { 58 | let c_comment = Comment::C( 59 | CommentStyle::Single("//".into()), 60 | Some(CommentStyle::Multiline { 61 | start: "/*".into(), 62 | between: "*".into(), 63 | end: "*/".into(), 64 | }), 65 | ); 66 | let py_comment = Comment::Python3(CommentStyle::Single("#".into()), None); 67 | let mysql_comment = Comment::MySQL(CommentStyle::Single("--".into()), None); 68 | 69 | let javascript_lang = Lang::Javascript(LangInfo { 70 | name: "javascript".to_string(), 71 | extension: "js".to_string(), 72 | comment: c_comment.clone(), 73 | }); 74 | let python_lang = Lang::Python3(LangInfo { 75 | name: "python3".to_string(), 76 | extension: "py".to_string(), 77 | comment: py_comment.clone(), 78 | }); 79 | 80 | match s { 81 | "rust" => Ok(Lang::Rust(LangInfo { 82 | name: "rust".to_string(), 83 | extension: "rs".to_string(), 84 | comment: c_comment, 85 | })), 86 | "java" => Ok(Lang::Java(LangInfo { 87 | name: "java".to_string(), 88 | extension: "java".to_string(), 89 | comment: c_comment, 90 | })), 91 | "js" => Ok(javascript_lang), 92 | "javascript" => Ok(javascript_lang), 93 | "python" => Ok(python_lang), 94 | "py" => Ok(python_lang), 95 | "python3" => Ok(python_lang), 96 | "cpp" => Ok(Lang::Cpp(LangInfo { 97 | name: "cpp".into(), 98 | extension: "cpp".into(), 99 | comment: c_comment, 100 | })), 101 | "mysql" => Ok(Lang::MySQL(LangInfo { 102 | name: "mysql".to_string(), 103 | extension: "sql".to_string(), 104 | comment: mysql_comment, 105 | })), 106 | "ruby" => Ok(Lang::Ruby(LangInfo { 107 | name: "ruby".to_string(), 108 | extension: "rb".to_string(), 109 | comment: py_comment, 110 | })), 111 | "rb" => Ok(Lang::Ruby(LangInfo { 112 | name: "ruby".to_string(), 113 | extension: "rb".to_string(), 114 | comment: py_comment, 115 | })), 116 | "c" => Ok(Lang::C(LangInfo { 117 | name: "c".into(), 118 | extension: "c".into(), 119 | comment: c_comment, 120 | })), 121 | "csharp" => Ok(Lang::CSharp(LangInfo { 122 | name: "csharp".into(), 123 | extension: "cs".into(), 124 | comment: c_comment, 125 | })), 126 | "cs" => Ok(Lang::CSharp(LangInfo { 127 | name: "csharp".into(), 128 | extension: "cs".into(), 129 | comment: c_comment, 130 | })), 131 | "golang" => Ok(Lang::Go(LangInfo { 132 | name: "golang".into(), 133 | extension: "go".into(), 134 | comment: c_comment, 135 | })), 136 | "go" => Ok(Lang::Go(LangInfo { 137 | name: "golang".into(), 138 | extension: "go".into(), 139 | comment: c_comment, 140 | })), 141 | "php" => Ok(Lang::Php(LangInfo { 142 | name: "php".into(), 143 | extension: "php".into(), 144 | comment: c_comment, 145 | })), 146 | "kotlin" => Ok(Lang::Kotlin(LangInfo { 147 | name: "kotlin".into(), 148 | extension: "kt".into(), 149 | comment: c_comment, 150 | })), 151 | "scala" => Ok(Lang::Scala(LangInfo { 152 | name: "scala".into(), 153 | extension: "scala".into(), 154 | comment: c_comment, 155 | })), 156 | "swift" => Ok(Lang::Swift(LangInfo { 157 | name: "swift".into(), 158 | extension: "swift".into(), 159 | comment: c_comment, 160 | })), 161 | "typescript" => Ok(Lang::Typescript(LangInfo { 162 | name: "typescript".into(), 163 | extension: "ts".into(), 164 | comment: c_comment, 165 | })), 166 | "ts" => Ok(Lang::Typescript(LangInfo { 167 | name: "typescript".into(), 168 | extension: "ts".into(), 169 | comment: c_comment, 170 | })), 171 | _ => Err(LeetUpError::Any(anyhow!("Language not supported!"))), 172 | } 173 | } 174 | } 175 | 176 | impl Lang { 177 | pub fn info(&self) -> LangInfo { 178 | match self.clone() { 179 | Lang::Rust(info) => info, 180 | Lang::Java(info) => info, 181 | Lang::Javascript(info) => info, 182 | Lang::Python3(info) => info, 183 | Lang::MySQL(info) => info, 184 | Lang::Cpp(info) => info, 185 | Lang::Ruby(info) => info, 186 | Lang::C(info) => info, 187 | Lang::CSharp(info) => info, 188 | Lang::Go(info) => info, 189 | Lang::Php(info) => info, 190 | Lang::Kotlin(info) => info, 191 | Lang::Scala(info) => info, 192 | Lang::Swift(info) => info, 193 | Lang::Typescript(info) => info, 194 | } 195 | } 196 | } 197 | 198 | impl<'de> Deserialize<'de> for Lang { 199 | fn deserialize(deserializer: D) -> Result 200 | where 201 | D: serde::Deserializer<'de>, 202 | { 203 | let s = String::deserialize(deserializer)?; 204 | Lang::from_str(&s).map_err(de::Error::custom) 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/service/leetcode.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ord; 2 | use std::collections::HashMap; 3 | use std::env; 4 | use std::fs::{self, File}; 5 | use std::io::{prelude::*, stdin}; 6 | use std::ops::Deref; 7 | use std::path::{Path, PathBuf}; 8 | 9 | use anyhow::anyhow; 10 | use async_trait::async_trait; 11 | use colci::Color; 12 | use html2text::from_read; 13 | use leetup_cache::kvstore::KvStore; 14 | use log::{debug, info}; 15 | use reqwest::header::{self, HeaderMap, HeaderValue}; 16 | use serde_json::{json, Value}; 17 | 18 | use crate::model::{ 19 | CodeDefinition, Problem, ProblemInfo, ProblemInfoSeq, StatStatusPair, SubmissionResponse, 20 | TopicTagQuestion, 21 | }; 22 | use crate::printer::SubmitExecutionResult; 23 | use crate::template::parse_code; 24 | use crate::{ 25 | client::RemoteClient, 26 | cmd::{self, List, OrderBy, Query, User}, 27 | printer::{Printer, TestExecutionResult}, 28 | service::{self, auth, CacheKey, Comment, CommentStyle, LangInfo, ServiceProvider, Session}, 29 | template::{InjectPosition, Pattern}, 30 | Config, Either, LeetUpError, Result, 31 | }; 32 | 33 | /// Leetcode holds all attributes required to implement ServiceProvider trait. 34 | pub struct Leetcode<'a> { 35 | /// Store user session 36 | /// 37 | /// If session is empty, user should be able to view problems. 38 | session: Option<&'a Session>, 39 | 40 | /// Get config from config.json 41 | config: &'a Config, 42 | 43 | /// Provides caching mechanism for OJ(Online Judge). 44 | cache: KvStore, 45 | 46 | /// Service provider name 47 | name: &'a str, 48 | 49 | remote_client: RemoteClient<'a>, 50 | } 51 | 52 | #[async_trait] 53 | impl<'a> ServiceProvider<'a> for Leetcode<'a> { 54 | fn session(&self) -> Option<&Session> { 55 | self.session 56 | } 57 | 58 | fn config(&self) -> Result<&Config> { 59 | Ok(self.config) 60 | } 61 | 62 | /// Fetch all problems 63 | /// 64 | /// Use cache wherever necessary 65 | async fn fetch_all_problems(&mut self) -> Result { 66 | let problems_res: Value; 67 | if let Some(ref val) = self.cache.get(CacheKey::Problems.into())? { 68 | debug!("Fetching problems from cache..."); 69 | problems_res = serde_json::from_str::(val)?; 70 | } else { 71 | let url = &self.config.urls.problems_all; 72 | let session = self.session(); 73 | problems_res = self 74 | .remote_client 75 | .get(url, None, session) 76 | .await? 77 | .json::() 78 | .await 79 | .map_err(LeetUpError::Reqwest)?; 80 | let res_serialized = serde_json::to_string(&problems_res)?; 81 | self.cache.set(CacheKey::Problems.into(), res_serialized)?; 82 | } 83 | 84 | Ok(problems_res) 85 | } 86 | 87 | async fn list_problems(&mut self, list: List) -> Result<()> { 88 | if !self.is_user_logged_in() { 89 | print!( 90 | "{}", 91 | Color::Red("You need to login to list problems").make() 92 | ); 93 | return Ok(()); 94 | } 95 | 96 | let problems_res = self.fetch_all_problems().await?; 97 | let mut probs: ProblemInfoSeq = vec![]; 98 | 99 | if let Some(ref tag) = list.tag { 100 | let tag_questions = self.get_problems_with_topic_tag(tag).await?["data"]["topicTag"] 101 | ["questions"] 102 | .clone(); 103 | let problems: Vec = serde_json::from_value(tag_questions)?; 104 | for prob in problems { 105 | probs.push(Box::new(prob)); 106 | } 107 | } else { 108 | let problems: Vec = 109 | serde_json::from_value(problems_res["stat_status_pairs"].clone())?; 110 | 111 | for prob in problems { 112 | probs.push(Box::new(prob)); 113 | } 114 | } 115 | 116 | if let Some(ref order) = list.order { 117 | let orders = OrderBy::from_str(order); 118 | probs.sort_by(|a, b| Leetcode::with_ordering(orders.as_slice(), a, b)); 119 | } else { 120 | probs.sort_by(Ord::cmp); 121 | } 122 | 123 | if list.query.is_some() || list.keyword.is_some() { 124 | let filter_predicate = |o: &Box| { 125 | let default_keyword = String::from(""); 126 | let keyword = list 127 | .keyword 128 | .as_ref() 129 | .unwrap_or(&default_keyword) 130 | .to_ascii_lowercase(); 131 | let has_keyword = o.question_title().to_lowercase().contains(&keyword); 132 | 133 | return list 134 | .query 135 | .as_ref() 136 | .map(|query| Query::from_str(query)) 137 | .map(|queries| Leetcode::apply_queries(&queries, o)) 138 | .map(|result| has_keyword && result) 139 | .unwrap_or(has_keyword); 140 | }; 141 | 142 | Leetcode::pretty_list( 143 | &probs 144 | .into_iter() 145 | .filter(filter_predicate) 146 | .collect::>>(), 147 | ); 148 | } else { 149 | Leetcode::pretty_list(probs.iter()); 150 | } 151 | 152 | Ok(()) 153 | } 154 | 155 | async fn pick_problem(&mut self, pick: cmd::Pick) -> Result<()> { 156 | let probs = self.fetch_problems().await?; 157 | let urls = &self.config.urls; 158 | let lang = pick 159 | .lang 160 | .as_ref() 161 | .map(|l| l.info()) 162 | .unwrap_or(self.config.lang.info()); 163 | 164 | let problem: Problem = probs 165 | .iter() 166 | .find(|item| { 167 | item.stat.frontend_question_id == pick.id.expect("Expected frontend_question_id") 168 | }) 169 | .map(|item| Problem { 170 | id: item.stat.frontend_question_id, 171 | link: format!("{}{}/", urls.problems, item.stat.question_title_slug), 172 | slug: item.stat.question_title_slug.to_string(), 173 | lang: lang.name.to_owned(), 174 | typed_code: None, 175 | }) 176 | .expect("Problem with given ID not found"); 177 | 178 | let problem_id = problem.id; 179 | let slug = problem.slug.to_owned(); 180 | let query = r#" 181 | query getQuestionDetail($titleSlug: String!) { 182 | question(titleSlug: $titleSlug) { 183 | content 184 | stats 185 | likes 186 | dislikes 187 | codeDefinition 188 | sampleTestCase 189 | enableRunCode 190 | metaData 191 | translatedContent 192 | } 193 | } 194 | "#; 195 | let body: Value = json!({ 196 | "query": query, 197 | "variables": json!({ 198 | "titleSlug": slug.to_owned(), 199 | }), 200 | "operationName": "getQuestionDetail" 201 | }); 202 | 203 | let response = self 204 | .remote_client 205 | .post(&urls.graphql, &body, || None) 206 | .await?; 207 | debug!("Response: {}", response); 208 | 209 | self.generate_problem_stub(&lang, &problem, problem_id, slug, &response)?; 210 | 211 | Ok(()) 212 | } 213 | 214 | async fn problem_test(&self, test: cmd::Test) -> Result<()> { 215 | let problem = service::extract_problem(test.filename)?; 216 | 217 | let test_data = self.get_test_data(test.test_data); 218 | debug!("Test data: {:?}", test_data); 219 | let typed_code = parse_code(problem.typed_code.as_ref().expect("Expected typed_code")); 220 | let body = json!({ 221 | "lang": problem.lang.to_owned(), 222 | "question_id": problem.id, 223 | "typed_code": typed_code, 224 | "data_input": test_data, 225 | "judge_type": "large" 226 | }); 227 | let url = &self.config()?.urls.test; 228 | debug!("problem_test url: {}, {:?}", url, body); 229 | let response = self.run_code(url, &problem, body).await; 230 | debug!("problem_test response: {:?}", response); 231 | 232 | match response { 233 | Err(e) => { 234 | println!("\n\n{}", Color::Red(e.to_string().as_str()).make()); 235 | println!( 236 | "\n{}", 237 | Color::Yellow("Note: If error status is 4XX, make sure you are logged in!") 238 | .make() 239 | ); 240 | } 241 | Ok(json) => { 242 | let url = self.config.urls.verify.replace( 243 | "$id", 244 | json["interpret_id"].as_str().ok_or_else(|| { 245 | LeetUpError::Any(anyhow!("Unable to replace `interpret_id`")) 246 | })?, 247 | ); 248 | let result: SubmissionResponse = 249 | serde_json::from_value(self.verify_run_code(&url).await?)?; 250 | let execution_result = TestExecutionResult::new(test_data.into(), result); 251 | execution_result.print(); 252 | } 253 | } 254 | 255 | Ok(()) 256 | } 257 | 258 | async fn problem_submit(&self, submit: cmd::Submit) -> Result<()> { 259 | let problem = service::extract_problem(submit.filename)?; 260 | let body = json!({ 261 | "lang": problem.lang.to_owned(), 262 | "question_id": problem.id, 263 | "test_mode": false, 264 | "typed_code": parse_code(problem.typed_code.as_ref().expect("Expected typed_code")), 265 | "judge_type": "large", 266 | }); 267 | let url = &self.config()?.urls.submit; 268 | let response = self.run_code(url, &problem, body).await?; 269 | let url = self 270 | .config 271 | .urls 272 | .verify 273 | .replace("$id", &response["submission_id"].to_string()); 274 | let result: SubmissionResponse = serde_json::from_value(self.verify_run_code(&url).await?)?; 275 | let execution_result = SubmitExecutionResult::new(result); 276 | execution_result.print(); 277 | Ok(()) 278 | } 279 | 280 | async fn process_auth(&mut self, user: User) -> Result<()> { 281 | // cookie login 282 | if user.cookie.is_some() { 283 | let session = auth::cookie_login(self).await?; 284 | self.cache_session(session)?; 285 | } 286 | 287 | if user.logout.is_some() { 288 | self.logout()?; 289 | println!("User logged out!"); 290 | } 291 | 292 | Ok(()) 293 | } 294 | 295 | fn cache(&mut self) -> Result<&KvStore> { 296 | Ok(&self.cache) 297 | } 298 | 299 | fn name(&self) -> &'a str { 300 | self.name 301 | } 302 | } 303 | 304 | impl<'a> Leetcode<'a> { 305 | pub fn new(session: Option<&'a Session>, config: &'a Config, cache: KvStore) -> Result { 306 | let name = "leetcode"; 307 | 308 | Ok(Leetcode { 309 | session, 310 | config, 311 | cache, 312 | name, 313 | remote_client: RemoteClient::new(config, session), 314 | }) 315 | } 316 | 317 | fn is_user_logged_in(&self) -> bool { 318 | self.cache.has_key(CacheKey::Session.into()) 319 | } 320 | 321 | fn cache_session(&mut self, session: Session) -> Result<()> { 322 | let session_str = serde_json::to_string(&session)?; 323 | self.cache.set(CacheKey::Session.into(), session_str)?; 324 | // remove key `problems`, rebuild problems cache. 325 | // 326 | // NOTE: cache.remove throws "Key not found" error 327 | // so ignore that error if it is thrown. 328 | if self.cache.remove(CacheKey::Problems.into()).is_err() {} 329 | Ok(()) 330 | } 331 | 332 | pub async fn fetch_problems(&mut self) -> Result> { 333 | let problems = self.fetch_all_problems().await?; 334 | let problems: Vec = 335 | serde_json::from_value(problems["stat_status_pairs"].clone())?; 336 | 337 | Ok(problems) 338 | } 339 | 340 | async fn run_code(&self, url: &str, problem: &Problem, body: Value) -> Result { 341 | let url = url.replace("$slug", &problem.slug); 342 | self.remote_client 343 | .post(&url, &body, || { 344 | let mut headers = HeaderMap::new(); 345 | headers.insert( 346 | header::REFERER, 347 | HeaderValue::from_str(&problem.link).expect("Link is required!"), 348 | ); 349 | Some(headers) 350 | }) 351 | .await 352 | } 353 | 354 | async fn verify_run_code(&self, url: &str) -> Result { 355 | loop { 356 | let response = self 357 | .remote_client 358 | .get(url, None, self.session()) 359 | .await? 360 | .json::() 361 | .await?; 362 | if response["state"] == "SUCCESS" { 363 | return Ok(response); 364 | } 365 | std::thread::sleep(std::time::Duration::from_millis(200)); 366 | } 367 | } 368 | 369 | fn write_code_fragment( 370 | &self, 371 | buf: &mut String, 372 | comment: &str, 373 | code_fragment: Option<&Either>, 374 | pos: InjectPosition, 375 | ) -> Result<()> { 376 | if let Some(either) = code_fragment { 377 | let inject_code_pos_pattern = format!( 378 | "\n{} {}\n", 379 | comment, 380 | Pattern::InjectCodePosition(pos).to_string() 381 | ); 382 | buf.push_str(&inject_code_pos_pattern); 383 | let code_fragment = either.to_string(); 384 | buf.push_str(&code_fragment); 385 | buf.push_str(&inject_code_pos_pattern); 386 | } 387 | Ok(()) 388 | } 389 | 390 | fn logout(&mut self) -> Result<()> { 391 | if self.cache.remove(CacheKey::Session.into()).is_err() { 392 | println!("User not logged in!"); 393 | return Ok(()); 394 | } 395 | if self.cache.remove(CacheKey::Problems.into()).is_err() {} 396 | Ok(()) 397 | } 398 | 399 | fn execute_script(&self, cmd: &str, problem: &Problem, dir: &Path) -> Result<()> { 400 | let dir_str = dir.to_str().expect("Expected a valid directory"); 401 | let cmd = cmd.replace(&Pattern::WorkingDir.to_string(), dir_str); 402 | let cmd = cmd.replace(&Pattern::Problem.to_string(), &problem.slug); 403 | std::process::Command::new("sh") 404 | .args(["-c", &cmd]) 405 | .spawn()? 406 | .wait()?; 407 | Ok(()) 408 | } 409 | 410 | fn pick_hook(&self, content: &str, problem: &Problem, lang: &LangInfo) -> Result<()> { 411 | let mut curr_dir = env::current_dir()?; 412 | let mut filename = curr_dir.clone(); 413 | let cfg = self.config()?; 414 | if let Some(ref cfg) = cfg.pick_hook { 415 | if let Some(hook_cfg) = cfg.get(&lang.name) { 416 | if let Some(dir) = hook_cfg.working_dir() { 417 | let dir = shellexpand::tilde(dir); 418 | curr_dir = PathBuf::from(dir.deref()); 419 | fs::create_dir_all(&curr_dir)?; 420 | filename = curr_dir.clone(); 421 | } 422 | if let Some(pre) = hook_cfg.script_pre_generation() { 423 | println!( 424 | "{}", 425 | Color::Cyan("Executing pre-generation script...").make() 426 | ); 427 | let cmd = pre.to_string(); 428 | self.execute_script(&cmd, problem, &curr_dir)?; 429 | } 430 | self.write_content(&mut filename, problem, lang, content.as_bytes())?; 431 | 432 | if let Some(post) = hook_cfg.script_post_generation() { 433 | println!( 434 | "{}", 435 | Color::Cyan("Executing post-generation script...").make() 436 | ); 437 | let cmd = post.to_string(); 438 | self.execute_script(&cmd, problem, &curr_dir)?; 439 | } 440 | 441 | // File path can be wrong if you used: `mkdir`, `cd`, `mv` to move 442 | // around the generated file. Find the right path used in your script! 443 | println!( 444 | "Generated: {}\n{}", 445 | Color::Magenta(filename.to_str().ok_or(LeetUpError::OptNone)?).make(), 446 | Color::Yellow("Note: File path can be wrong if you used: `mkdir`, `cd`, `mv` to move around the generated file. Find the right path used in your script!").make() 447 | ); 448 | return Ok(()); 449 | } 450 | } 451 | self.write_content(&mut filename, problem, lang, content.as_bytes())?; 452 | println!( 453 | "Generated: {}", 454 | Color::Magenta(filename.to_str().ok_or(LeetUpError::OptNone)?).make() 455 | ); 456 | 457 | Ok(()) 458 | } 459 | 460 | fn write_content( 461 | &self, 462 | filename: &mut PathBuf, 463 | problem: &Problem, 464 | lang: &LangInfo, 465 | content: &[u8], 466 | ) -> Result<()> { 467 | filename.push(&problem.slug); 468 | filename.set_extension(&lang.extension); 469 | 470 | let mut file = File::create(&filename)?; 471 | file.write_all(content)?; 472 | Ok(()) 473 | } 474 | 475 | async fn get_problems_with_topic_tag(&self, tag: &str) -> Result { 476 | let query = r#" 477 | query getTopicTag($slug: String!) { 478 | topicTag(slug: $slug) { 479 | name 480 | slug 481 | questions { 482 | difficulty 483 | isPaidOnly 484 | title 485 | titleSlug 486 | questionFrontendId 487 | status 488 | } 489 | } 490 | } 491 | "#; 492 | let body: Value = json!({ 493 | "operationName": "getTopicTag", 494 | "variables": { 495 | "slug": tag, 496 | }, 497 | "query": query 498 | }); 499 | 500 | self.remote_client 501 | .post(&self.config.urls.graphql, &body, || None) 502 | .await 503 | } 504 | 505 | fn generate_problem_stub( 506 | &mut self, 507 | lang: &LangInfo, 508 | problem: &Problem, 509 | problem_id: usize, 510 | slug: String, 511 | response: &Value, 512 | ) -> Result<()> { 513 | let mut definition = None; 514 | let mut start_comment = ""; 515 | let line_comment; 516 | let mut end_comment = ""; 517 | let single_comment; 518 | 519 | match &lang.comment { 520 | Comment::C(CommentStyle::Single(s), multi) => { 521 | single_comment = s; 522 | if let Some(CommentStyle::Multiline { 523 | start, 524 | between, 525 | end, 526 | }) = multi 527 | { 528 | start_comment = start.as_str(); 529 | line_comment = between.as_str(); 530 | end_comment = end.as_str(); 531 | } else { 532 | line_comment = single_comment; 533 | } 534 | } 535 | Comment::Python3(CommentStyle::Single(s), _) 536 | | Comment::MySQL(CommentStyle::Single(s), _) => { 537 | line_comment = s; 538 | single_comment = s; 539 | } 540 | _ => unreachable!(), 541 | }; 542 | 543 | if let Some(content) = &response["data"]["question"]["content"].as_str() { 544 | let content = from_read(content.as_bytes(), 80); 545 | let content = content.replace("**", ""); 546 | let content = content 547 | .split('\n') 548 | .map(|s| format!("{} {}", line_comment, s)) 549 | .collect::>() 550 | .join("\n"); 551 | info!("Single Comment: {}", single_comment); 552 | 553 | let pattern_custom = format!("{} {}", single_comment, Pattern::CustomCode.to_string()); 554 | let pattern_leetup_info = 555 | format!("{} {}", single_comment, Pattern::LeetUpInfo.to_string()); 556 | let content = format!( 557 | "{}\n{} id={} lang={} slug={}\n\n{}\n{}\n{}\n{}", 558 | pattern_custom, 559 | pattern_leetup_info, 560 | problem_id, 561 | lang.name, 562 | slug, 563 | start_comment, 564 | content, 565 | end_comment, 566 | pattern_custom 567 | ); 568 | debug!("Content: {}", content); 569 | definition = Some(content); 570 | } 571 | 572 | let mut filename = env::current_dir()?; 573 | filename.push(slug); 574 | filename.set_extension(&lang.extension); 575 | 576 | if let Some(code_defs) = &response["data"]["question"]["codeDefinition"].as_str() { 577 | let mut buf = String::new(); 578 | let code_defs: HashMap<_, _> = serde_json::from_str::>(code_defs)? 579 | .into_iter() 580 | .map(|def| (def.value.to_owned(), def)) 581 | .into_iter() 582 | .collect(); 583 | if let Some(ref definition) = definition { 584 | buf.push_str(definition) 585 | } 586 | let pattern_code = format!("\n{} {}\n", single_comment, Pattern::Code.to_string()); 587 | let code = &code_defs 588 | .get(&lang.name) 589 | .ok_or(LeetUpError::OptNone)? 590 | .default_code; 591 | debug!("Code: {}", code); 592 | let inject_code = self 593 | .config()? 594 | .inject_code 595 | .as_ref() 596 | .and_then(|c| c.get(&problem.lang)); 597 | debug!("InjectCode: {:#?}", inject_code); 598 | if let Some(inject_code) = inject_code { 599 | self.write_code_fragment( 600 | &mut buf, 601 | single_comment, 602 | inject_code.before_code_exclude.as_ref(), 603 | InjectPosition::BeforeCodeExclude, 604 | )?; 605 | } 606 | buf.push_str(&pattern_code); 607 | if let Some(inject_code) = inject_code { 608 | self.write_code_fragment( 609 | &mut buf, 610 | single_comment, 611 | inject_code.before_code.as_ref(), 612 | InjectPosition::BeforeCode, 613 | )?; 614 | } 615 | buf.push('\n'); 616 | buf.push_str(code); 617 | buf.push_str(&pattern_code); 618 | if let Some(inject_code) = inject_code { 619 | self.write_code_fragment( 620 | &mut buf, 621 | single_comment, 622 | inject_code.after_code.as_ref(), 623 | InjectPosition::AfterCode, 624 | )?; 625 | } 626 | 627 | self.pick_hook(&buf, problem, lang)?; 628 | } 629 | 630 | Ok(()) 631 | } 632 | 633 | /* 634 | * Parse Option> from structopt 635 | * 636 | * Get string from command line if provided, otherwise try to get string from stdin 637 | * 638 | * We can provide test data as multiline input using stdin. 639 | * 640 | * # Example: 641 | * ```bash 642 | * leetup test 3sum.java -t << END 643 | * [1,-1,0] 644 | * [0, 1, 1, 1, 2, -3, -1] 645 | * [1,2,3] 646 | * END 647 | * ``` 648 | */ 649 | fn get_test_data(&self, test_data: Option>) -> String { 650 | test_data.unwrap().unwrap_or_else(|| { 651 | let mut buf = String::new(); 652 | stdin() 653 | .lock() 654 | .read_to_string(&mut buf) 655 | .expect("test input expected from stdin"); 656 | buf 657 | }) 658 | } 659 | } 660 | -------------------------------------------------------------------------------- /src/service/mod.rs: -------------------------------------------------------------------------------- 1 | pub use file::*; 2 | pub use lang::*; 3 | pub use provider::*; 4 | pub use session::*; 5 | 6 | pub mod auth; 7 | mod file; 8 | mod lang; 9 | pub mod leetcode; 10 | mod pool; 11 | mod provider; 12 | mod session; 13 | -------------------------------------------------------------------------------- /src/service/pool.rs: -------------------------------------------------------------------------------- 1 | use crate::Result; 2 | use std::sync::{mpsc, Arc, Mutex}; 3 | use std::thread; 4 | 5 | pub trait ThreadPool { 6 | ///Creates a new thread pool, immediately spawning the specified number of threads. 7 | /// 8 | /// Returns an error if any thread fails to spawn. All previously-spawned 9 | /// threads are terminated. 10 | fn new(threads: u32) -> Result 11 | where 12 | Self: Sized; 13 | 14 | /// Spawn a function into the threadpool. 15 | /// 16 | /// Spawning always succeeds, but if the function panics the threadpool continues to operate with the 17 | /// same number of threads — the thread count is not reduced nor is 18 | /// the thread pool destroyed, corrupted or invalidated. 19 | fn spawn(&self, job: F) 20 | where 21 | F: FnOnce() + Send + 'static; 22 | } 23 | 24 | enum Message { 25 | NewJob(Job), 26 | Terminate, 27 | } 28 | 29 | type Job = Box; 30 | type Receiver = Arc>>; 31 | 32 | #[derive(Clone)] 33 | struct JobReceiver(Receiver); 34 | 35 | impl Drop for JobReceiver { 36 | fn drop(&mut self) { 37 | if thread::panicking() { 38 | let rx = self.clone(); 39 | 40 | if let Err(e) = thread::Builder::new().spawn(move || execute_job(rx)) { 41 | eprint!("Failed to spawn a thread: {}", e); 42 | } 43 | } 44 | } 45 | } 46 | 47 | fn execute_job(worker: JobReceiver) { 48 | loop { 49 | if let Ok(rx) = worker.0.lock() { 50 | if let Ok(msg) = rx.recv() { 51 | match msg { 52 | Message::NewJob(job) => job(), 53 | Message::Terminate => break, 54 | } 55 | } else { 56 | break; 57 | } 58 | } else { 59 | break; 60 | } 61 | } 62 | } 63 | 64 | pub struct SharedQueueThreadPool { 65 | size: u32, 66 | sender: mpsc::Sender, 67 | } 68 | 69 | impl ThreadPool for SharedQueueThreadPool { 70 | fn new(size: u32) -> Result { 71 | assert!(size > 0); 72 | 73 | let (sender, receiver) = mpsc::channel::(); 74 | 75 | let receiver = Arc::new(Mutex::new(receiver)); 76 | 77 | for _ in 0..size as usize { 78 | let rx = receiver.clone(); 79 | 80 | thread::Builder::new().spawn(move || execute_job(JobReceiver(rx)))?; 81 | } 82 | 83 | Ok(SharedQueueThreadPool { sender, size }) 84 | } 85 | 86 | fn spawn(&self, f: F) 87 | where 88 | F: FnOnce() + Send + 'static, 89 | { 90 | let job = Box::new(f); 91 | 92 | self.sender 93 | .send(Message::NewJob(job)) 94 | .expect("The thread pool has no thread."); 95 | } 96 | } 97 | 98 | impl Drop for SharedQueueThreadPool { 99 | fn drop(&mut self) { 100 | for _ in 0..self.size { 101 | match self.sender.send(Message::Terminate) { 102 | Ok(_) => println!("Worker terminated!"), 103 | Err(e) => eprintln!("{}", e), 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/service/provider.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | 3 | use ansi_term::Colour::{Green, Red, Yellow}; 4 | use async_trait::async_trait; 5 | use leetup_cache::kvstore::KvStore; 6 | 7 | use crate::model::DifficultyType::{Easy, Hard, Medium}; 8 | use crate::model::{DifficultyType, ProblemInfo}; 9 | use crate::service::Session; 10 | use crate::{ 11 | cmd::{self, OrderBy, Query, User}, 12 | icon::Icon, 13 | Config, Result, 14 | }; 15 | 16 | /// ServiceProvider trait provides all the functionalities required to solve problems 17 | /// on any type of Online Judge through leetup CLI. 18 | #[async_trait] 19 | pub trait ServiceProvider<'a> { 20 | fn session(&self) -> Option<&Session>; 21 | fn config(&self) -> Result<&Config>; 22 | async fn fetch_all_problems(&mut self) -> Result; 23 | async fn list_problems(&mut self, list: cmd::List) -> Result<()>; 24 | async fn pick_problem(&mut self, pick: cmd::Pick) -> Result<()>; 25 | async fn problem_test(&self, test: cmd::Test) -> Result<()>; 26 | async fn problem_submit(&self, submit: cmd::Submit) -> Result<()>; 27 | async fn process_auth(&mut self, user: User) -> Result<()>; 28 | fn cache(&mut self) -> Result<&KvStore>; 29 | fn name(&self) -> &'a str; 30 | 31 | /// Print list of problems properly. 32 | fn pretty_list>>(probs: T) { 33 | for prob in probs { 34 | let is_favorite = if let Some(is_favor) = prob.is_favorite() { 35 | is_favor 36 | } else { 37 | false 38 | }; 39 | let starred_icon = if is_favorite { 40 | Yellow.paint(Icon::Star.to_string()).to_string() 41 | } else { 42 | Icon::Empty.to_string() 43 | }; 44 | 45 | let locked_icon = if prob.is_paid_only() { 46 | Red.paint(Icon::Lock.to_string()).to_string() 47 | } else { 48 | Icon::Empty.to_string() 49 | }; 50 | 51 | let acd = if prob.status().is_some() { 52 | Green.paint(Icon::Yes.to_string()).to_string() 53 | } else { 54 | Icon::Empty.to_string() 55 | }; 56 | 57 | println!( 58 | "{} {:2} {} [{:^4}] {:75} {:6}", 59 | starred_icon, 60 | locked_icon, 61 | acd, 62 | prob.question_id(), 63 | prob.question_title(), 64 | prob.difficulty().to_string() 65 | ); 66 | } 67 | } 68 | 69 | /// Filter problems using multiple queries. 70 | fn apply_queries(queries: &Vec, o: &Box) -> bool { 71 | let mut is_satisfied = true; 72 | let difficulty: DifficultyType = o.difficulty().into(); 73 | let is_favorite = if let Some(is_favor) = o.is_favorite() { 74 | is_favor 75 | } else { 76 | false 77 | }; 78 | 79 | for q in queries { 80 | match q { 81 | Query::Easy => is_satisfied &= difficulty == Easy, 82 | Query::NotEasy => is_satisfied &= difficulty != Easy, 83 | Query::Medium => is_satisfied &= difficulty == Medium, 84 | Query::NotMedium => is_satisfied &= difficulty != Medium, 85 | Query::Hard => is_satisfied &= difficulty == Hard, 86 | Query::NotHard => is_satisfied &= difficulty != Hard, 87 | Query::Locked => is_satisfied &= o.is_paid_only(), 88 | Query::Unlocked => is_satisfied &= !o.is_paid_only(), 89 | Query::Done => is_satisfied &= o.status().is_some(), 90 | Query::NotDone => is_satisfied &= o.status().is_none(), 91 | Query::Starred => is_satisfied &= is_favorite, 92 | Query::Unstarred => is_satisfied &= !is_favorite, 93 | } 94 | } 95 | 96 | is_satisfied 97 | } 98 | 99 | /// Order problems by Id, Title, Difficulty in Ascending or Descending order 100 | fn with_ordering( 101 | orders: &[OrderBy], 102 | a: &Box, 103 | b: &Box, 104 | ) -> Ordering { 105 | let mut ordering = Ordering::Equal; 106 | let id_ordering = a.question_id().cmp(&b.question_id()); 107 | let title_ordering = a.question_title().cmp(&b.question_title()); 108 | let a_difficulty_level: DifficultyType = a.difficulty().into(); 109 | let b_difficulty_level: DifficultyType = b.difficulty().into(); 110 | let diff_ordering = a_difficulty_level.cmp(&b_difficulty_level); 111 | 112 | for order in orders { 113 | match order { 114 | OrderBy::IdAsc => ordering = ordering.then(id_ordering), 115 | OrderBy::IdDesc => ordering = ordering.then(id_ordering.reverse()), 116 | OrderBy::TitleAsc => ordering = ordering.then(title_ordering), 117 | OrderBy::TitleDesc => ordering = ordering.then(title_ordering.reverse()), 118 | OrderBy::DifficultyAsc => ordering = ordering.then(diff_ordering), 119 | OrderBy::DifficultyDesc => ordering = ordering.then(diff_ordering.reverse()), 120 | } 121 | } 122 | 123 | ordering 124 | } 125 | } 126 | 127 | pub enum CacheKey<'a> { 128 | Session, 129 | Problems, 130 | Problem(&'a str), 131 | } 132 | 133 | impl<'a> From> for String { 134 | fn from(key: CacheKey) -> Self { 135 | match key { 136 | CacheKey::Session => "session".to_string(), 137 | CacheKey::Problems => "problems".to_string(), 138 | CacheKey::Problem(id) => format!("problem_{}", id), 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/service/session.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use cookie::Cookie; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[derive(Debug, Clone, Serialize, Deserialize, Default)] 7 | pub struct Session { 8 | pub id: String, 9 | pub csrf: String, 10 | } 11 | 12 | impl Session { 13 | pub fn new(id: String, csrf: String) -> Self { 14 | Session { id, csrf } 15 | } 16 | } 17 | 18 | impl FromStr for Session { 19 | type Err = cookie::ParseError; 20 | 21 | fn from_str(raw: &str) -> std::result::Result { 22 | let raw_split = raw.split("; "); 23 | 24 | let cookies = raw_split.map(Cookie::parse).collect::>(); 25 | let mut id = String::new(); 26 | let mut csrf = String::new(); 27 | 28 | for cookie in cookies { 29 | let cookie = cookie?; 30 | let name = cookie.name(); 31 | match name { 32 | "LEETCODE_SESSION" => id = cookie.value().to_string(), 33 | "csrftoken" => csrf = cookie.value().to_string(), 34 | _ => (), 35 | } 36 | } 37 | 38 | Ok(Session { id, csrf }) 39 | } 40 | } 41 | 42 | fn session_to_cookie(id: &str, csrf: &str) -> String { 43 | let mut s = String::new(); 44 | s.push_str(&format!("{}={}; ", "LEETCODE_SESSION", id)); 45 | s.push_str(&format!("{}={}", "csrftoken", csrf)); 46 | 47 | s 48 | } 49 | 50 | impl From for String { 51 | fn from(session: Session) -> Self { 52 | session_to_cookie(&session.id, &session.csrf) 53 | } 54 | } 55 | 56 | impl From<&Session> for String { 57 | fn from(session: &Session) -> Self { 58 | session_to_cookie(&session.id, &session.csrf) 59 | } 60 | } 61 | 62 | #[test] 63 | fn test_cookie_parser() { 64 | let cookie = "csrftoken=asdsd; LEETCODE_SESSION=asdasd"; 65 | let session: Session = Session::from_str(cookie).unwrap(); 66 | 67 | assert!(!session.csrf.is_empty()); 68 | assert!(!session.id.is_empty()); 69 | } 70 | -------------------------------------------------------------------------------- /src/template.rs: -------------------------------------------------------------------------------- 1 | #[derive(Copy, Clone)] 2 | pub enum Pattern { 3 | LeetUpInfo, 4 | CustomCode, 5 | Code, 6 | InjectCodePosition(InjectPosition), 7 | Problem, 8 | // Problem name e.g. two-sum 9 | WorkingDir, 10 | } 11 | 12 | #[derive(Copy, Clone)] 13 | pub enum InjectPosition { 14 | BeforeCode, 15 | // Helpful for imports 16 | BeforeCodeExclude, 17 | AfterCode, 18 | BeforeFunctionDefinition, 19 | } 20 | 21 | impl From for String { 22 | fn from(p: Pattern) -> Self { 23 | match p { 24 | Pattern::LeetUpInfo => "@leetup=info".into(), 25 | Pattern::CustomCode => "@leetup=custom".into(), 26 | Pattern::Code => "@leetup=code".into(), 27 | Pattern::InjectCodePosition(pos) => match pos { 28 | InjectPosition::BeforeCode => "@leetup=inject:before_code".into(), 29 | InjectPosition::BeforeCodeExclude => "@leetup=inject:before_code_ex".into(), 30 | InjectPosition::AfterCode => "@leetup=inject:after_code".into(), 31 | InjectPosition::BeforeFunctionDefinition => { 32 | "@leetup=inject:before_function_definition".into() 33 | } 34 | }, 35 | Pattern::Problem => "@leetup=problem".into(), 36 | Pattern::WorkingDir => "@leetup=working_dir".into(), 37 | } 38 | } 39 | } 40 | 41 | impl<'a> From<&'a Pattern> for String { 42 | fn from(p: &Pattern) -> Self { 43 | String::from(*p) 44 | } 45 | } 46 | 47 | impl ToString for Pattern { 48 | fn to_string(&self) -> String { 49 | String::from(*self) 50 | } 51 | } 52 | 53 | /// Parse code to submit only the relevant chunk of code. 54 | /// 55 | /// Ignore generated code definition and custom injected code for 56 | /// testing purposes. 57 | pub fn parse_code(code: &str) -> Option { 58 | let code_pattern: String = Pattern::Code.into(); 59 | let len = code_pattern.len(); 60 | 61 | let start_index = code 62 | .find(&code_pattern) 63 | .map(|index| index + len) 64 | .unwrap_or(0); 65 | 66 | let code = code.get(start_index..)?; 67 | 68 | let end_index = match code.find(&code_pattern) { 69 | Some(index) => { 70 | let code = &code[..index]; 71 | let index = code.rfind('\n').unwrap(); 72 | index + 1 73 | } 74 | None => code.len(), 75 | }; 76 | let code = code.get(..end_index)?; 77 | 78 | Some(code.into()) 79 | } 80 | 81 | #[test] 82 | fn test_parse_with_comments() { 83 | let code = r#" 84 | // @leetup=custom 85 | // Given an array of integers `nums` and an integer `target`, return *indices of 86 | // the two numbers such that they add up to `target`*. 87 | // 88 | // You may assume that each input would have *exactly* one solution, and you 89 | // may not use the *same* element twice. 90 | // 91 | // You can return the answer in any order. 92 | // 93 | // 94 | // Example 1: 95 | // 96 | // Input: nums = [2,7,11,15], target = 9 97 | // Output: [0,1] 98 | // Output: Because nums[0] + nums[1] == 9, we return [0, 1]. 99 | // 100 | // Example 2: 101 | // 102 | // Input: nums = [3,2,4], target = 6 103 | // Output: [1,2] 104 | // 105 | // Example 3: 106 | // 107 | // Input: nums = [3,3], target = 6 108 | // Output: [0,1] 109 | // 110 | // 111 | // Constraints: 112 | // 113 | // * `2 <= nums.length <= 105` 114 | // * `-109 <= nums[i] <= 109` 115 | // * `-109 <= target <= 109` 116 | // * Only one valid answer exists. 117 | // @leetup=custom 118 | 119 | // @leetup=code 120 | use std::collections::HashMap; 121 | 122 | impl Solution { 123 | pub fn two_sum(nums: Vec, target: i32) -> Vec { 124 | let mut index_map = HashMap::new(); 125 | 126 | for (i, num) in nums.iter().enumerate() { 127 | let y = target - num; 128 | if let Some(&idx) = index_map.get(&y) { 129 | return vec![idx as i32, i as i32]; 130 | } 131 | 132 | index_map.insert(num, i); 133 | } 134 | 135 | vec![] 136 | } 137 | } 138 | // @leetup=code 139 | "#; 140 | 141 | let expected_code = r#" 142 | use std::collections::HashMap; 143 | 144 | impl Solution { 145 | pub fn two_sum(nums: Vec, target: i32) -> Vec { 146 | let mut index_map = HashMap::new(); 147 | 148 | for (i, num) in nums.iter().enumerate() { 149 | let y = target - num; 150 | if let Some(&idx) = index_map.get(&y) { 151 | return vec![idx as i32, i as i32]; 152 | } 153 | 154 | index_map.insert(num, i); 155 | } 156 | 157 | vec![] 158 | } 159 | } 160 | "#; 161 | 162 | let actual_code = parse_code(code); 163 | assert_eq!(actual_code, Some(expected_code.into())); 164 | } 165 | 166 | #[test] 167 | fn test_parse_just_code() { 168 | let code = r#" 169 | use std::collections::HashMap; 170 | 171 | impl Solution { 172 | pub fn two_sum(nums: Vec, target: i32) -> Vec { 173 | let mut index_map = HashMap::new(); 174 | 175 | for (i, num) in nums.iter().enumerate() { 176 | let y = target - num; 177 | if let Some(&idx) = index_map.get(&y) { 178 | return vec![idx as i32, i as i32]; 179 | } 180 | 181 | index_map.insert(num, i); 182 | } 183 | 184 | vec![] 185 | } 186 | } 187 | "#; 188 | 189 | let expected_code = r#" 190 | use std::collections::HashMap; 191 | 192 | impl Solution { 193 | pub fn two_sum(nums: Vec, target: i32) -> Vec { 194 | let mut index_map = HashMap::new(); 195 | 196 | for (i, num) in nums.iter().enumerate() { 197 | let y = target - num; 198 | if let Some(&idx) = index_map.get(&y) { 199 | return vec![idx as i32, i as i32]; 200 | } 201 | 202 | index_map.insert(num, i); 203 | } 204 | 205 | vec![] 206 | } 207 | } 208 | "#; 209 | 210 | let actual_code = parse_code(code); 211 | assert_eq!(actual_code, Some(expected_code.into())); 212 | } 213 | -------------------------------------------------------------------------------- /tag.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Get the desired version bump type from command line argument 4 | if [ $# -ne 1 ]; then 5 | echo "Usage: $0 [major|minor|patch]" 6 | exit 1 7 | fi 8 | 9 | VERSION_BUMP="$1" 10 | 11 | # Get the current version from git tags or set to 1.0.0 if no tags exist 12 | CURRENT_VERSION=$(git describe --abbrev=0 --tags 2>/dev/null || echo "v1.0.0") 13 | CURRENT_VERSION=${CURRENT_VERSION:1} 14 | 15 | echo "Current Version: $CURRENT_VERSION" 16 | 17 | # Split the current version into an array using '.' as a delimiter 18 | IFS='.' read -ra CURRENT_VERSION_PARTS <<< "$CURRENT_VERSION" 19 | 20 | # Extract major, minor, and patch version numbers 21 | VNUM1=${CURRENT_VERSION_PARTS[0]} 22 | VNUM2=${CURRENT_VERSION_PARTS[1]} 23 | VNUM3=${CURRENT_VERSION_PARTS[2]} 24 | 25 | # Increment the version based on the bump type 26 | if [ "$VERSION_BUMP" == 'major' ]; then 27 | VNUM1=$((VNUM1+1)) 28 | VNUM2=0 29 | VNUM3=0 30 | elif [ "$VERSION_BUMP" == 'minor' ]; then 31 | VNUM2=$((VNUM2+1)) 32 | VNUM3=0 33 | elif [ "$VERSION_BUMP" == 'patch' ]; then 34 | VNUM3=$((VNUM3+1)) 35 | else 36 | echo "Invalid version bump type. Use 'major', 'minor', or 'patch'." 37 | exit 1 38 | fi 39 | 40 | # Create the new version number 41 | NEW_TAG="v$VNUM1.$VNUM2.$VNUM3" 42 | echo "Bumping $VERSION_BUMP version to $NEW_TAG" 43 | 44 | # Create a new tag and push it to the remote repository 45 | git tag "$NEW_TAG" 46 | git push --tags 47 | git push 48 | 49 | exit 0 50 | 51 | -------------------------------------------------------------------------------- /tests/cli.rs: -------------------------------------------------------------------------------- 1 | use std::process::Command; 2 | 3 | use assert_cmd::prelude::*; 4 | use predicates::str::contains; 5 | 6 | #[cfg(test)] 7 | mod tests { 8 | use std::fs::File; 9 | use std::io::Read; 10 | 11 | use super::*; 12 | 13 | #[test] 14 | fn cli_version() { 15 | Command::cargo_bin("leetup") 16 | .unwrap() 17 | .args(["-V"]) 18 | .assert() 19 | .stdout(contains(env!("CARGO_PKG_VERSION"))); 20 | } 21 | 22 | fn _get_id(problem: &str) -> usize { 23 | println!("{}", problem); 24 | let start_index = problem.find(" [").unwrap(); 25 | let end_index = problem.find(']').unwrap(); 26 | let id = problem.get(start_index + 2..end_index).unwrap().trim(); 27 | println!("{}", id); 28 | id.parse().unwrap() 29 | } 30 | 31 | fn _list_problems() { 32 | let bytes: Vec = Command::cargo_bin("leetup") 33 | .unwrap() 34 | .args(["list", "-oi"]) 35 | .assert() 36 | .get_output() 37 | .stdout 38 | .clone(); 39 | let result: Vec = String::from_utf8(bytes) 40 | .unwrap() 41 | .split('\n') 42 | .map(String::from) 43 | .collect(); 44 | 45 | let n = result.len() - 1; 46 | 47 | // Test OrderBy works by check first and last id 48 | // 49 | // NOTE: For some reason, result.last() is empty! 50 | assert_eq!(1, _get_id(result.get(0).as_ref().unwrap())); 51 | assert_eq!(n, _get_id(result.get(n - 1).as_ref().unwrap())); 52 | } 53 | 54 | #[ignore = "Not passing in CI -- works locally"] 55 | #[allow(dead_code)] 56 | fn pick_problem_lang_rust() { 57 | let bytes: Vec = Command::cargo_bin("leetup") 58 | .unwrap() 59 | .args(["pick", "-l", "rust", "1"]) 60 | .assert() 61 | .get_output() 62 | .stdout 63 | .clone(); 64 | let stripped_output = strip_ansi_escapes::strip(bytes); 65 | let generated_path = String::from_utf8(stripped_output) 66 | .unwrap() 67 | .replace("Generated: ", ""); 68 | let result = generated_path.trim_end(); 69 | let mut generated_file = File::open(result).unwrap(); 70 | let mut buffer = String::new(); 71 | generated_file.read_to_string(&mut buffer).unwrap(); 72 | assert!(buffer.contains("// @leetup=custom\n// @leetup=info id=1 lang=rust slug=two-sum")); 73 | assert!(buffer.contains("// @leetup=code\n")); 74 | } 75 | } 76 | --------------------------------------------------------------------------------