diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..e982631 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,4 @@ +[alias] +i = "install --path . --features=tool" +g = "run --features=tool -- leet gen" +t = "test --all-features" diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..4ca595c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,43 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: 'bug' +assignees: '' + +--- + +**Describe the bug** + + + +**Leetcode problem** + + + +**To Reproduce** + + + +`` + +**Error Message** + + + +``` +``` + +**Expected behavior** + + + +**Environment Info:** + + + +- Cargo Version: + +**Additional context** + + diff --git a/.github/ISSUE_TEMPLATE/missing_type.md b/.github/ISSUE_TEMPLATE/missing_type.md new file mode 100644 index 0000000..a82784c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/missing_type.md @@ -0,0 +1,19 @@ +--- +name: Missing Type +about: Report the missing type so we can try to add it +title: '' +labels: 'bug' +assignees: '' + +--- + +**Leetcode problem** + + + +**Error Message** + + + +``` +``` diff --git a/.github/workflows/general.yml b/.github/workflows/general.yml index 1abddc2..347d850 100644 --- a/.github/workflows/general.yml +++ b/.github/workflows/general.yml @@ -22,7 +22,7 @@ jobs: - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - name: Run tests - run: cargo test + run: cargo test --all-features fmt: name: Rustfmt diff --git a/.github/workflows/release_check.yml b/.github/workflows/release_check.yml new file mode 100644 index 0000000..4498453 --- /dev/null +++ b/.github/workflows/release_check.yml @@ -0,0 +1,23 @@ +name: Release Build Confirmation + +on: + push: + branches: + - main + pull_request: + types: [ opened, synchronize, reopened ] + branches: + - main +env: + CARGO_TERM_COLOR: always + +jobs: + release_compile: + name: Release Compile + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Run Release Compile + run: cargo check --all-features --release \ No newline at end of file diff --git a/.gitignore b/.gitignore index e69de29..ea8c4bf 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..2e460b9 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "cSpell.words": [ + "Vecbool", + "Veci" + ], + "editor.formatOnSave": true, + "files.autoSave": "onFocusChange", + "rust-analyzer.cargo.features": "all" // Sets the features used by rust analyzer +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..738c6ea --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,198 @@ +# Contributing + +## Where to start + +All contributions, bug reports, bug fixes, documentation improvements, enhancements, and ideas are welcome. + +The best place to start is to check the [issues](https://github.com/rust-practice/cargo-leet) +for something that interests you. +There are also other options in [discussions](https://github.com/rust-practice/cargo-leet/discussions), so feel free to pick from there as well. + +## Bug Reports + +Please see the [issue templates](https://github.com/rust-practice/cargo-leet/issues/new/choose) that describe the types of information that we are looking for but no worries just fill it the best you can and we'll go from there. + +## Working on the code + +### Fork the project + +In order to work on the project you will need your own fork. To do this click the "Fork" button on +this project. + +Once the project is forked you can work on it directly in github codespaces without needing to install anything by clicking the green button near the top and switching to code spaces. +According to [github docs](https://docs.github.com/en/codespaces/overview#billing-for-codespaces) by default you can only use it up to the free amount so you don't need to worry about charges. +Feel free to check some [notes collected](https://c-git.github.io/github/codespaces/) on how to use codespaces with rust (You don't need trunk for this project). + +Alternatively you can clone it to your local machine. The following commands creates the directory cargo-leet and connects your repository to the upstream (main project) repository. + +```sh +git clone https://github.com/your-user-name/cargo-leet.git +cd cargo-leet +git remote add upstream git@github.com:rust-practice/cargo-leet.git +``` + +### Creating a branch + +You want your main branch to reflect only production-ready code. +Please base your branch on the develop branch which is the default in cargo-leet repo so create a feature branch for +making your changes. +For example: + +```sh +git checkout -b my-new-feature +``` + +This changes your working directory to the my-new-feature branch. Keep any changes in this branch +specific to one bug or feature so the purpose is clear. You can have many my-new-features and switch +in between them using the git checkout command. + +When creating this branch, make sure your develop branch is up to date with the latest upstream +develop version. To update your local develop branch, you can do: + +```sh +git checkout develop +git pull upstream develop --ff-only +``` + +### Code linting, formatting, and tests + +You can run linting on your code at any time with: + +```sh +cargo clippy +``` + +To format the code run: + +```sh +cargo fmt +``` + +To run the tests: + +Note the following is overridden in `.cargo/config.toml` to run with all features enabled + +```sh +cargo t +``` + +Please run these checks before submitting your pull request. + +## Committing your code + +Once you have made changes to the code on your branch you can see which files have changed by running: + +```sh +git status +``` + +If new files were created that and are not tracked by git they can be added by running: + +```sh +git add . +``` + +Now you can commit your changes in your local repository: + +```sh +git commit -am 'Some short helpful message to describe your changes' +``` + +## Push your changes + +Once your changes are ready and all linting/tests are passing you can push your changes to your forked repository: + +```sh +git push origin my-new-feature +``` + +origin is the default name of your remote repository on GitHub. You can see all of your remote repositories by running: + +```sh +git remote -v +``` + +## Making a Pull Request + +After pushing your code to origin it is now on GitHub but not yet part of the cargo-leet project. +When you’re ready to ask for a code review, file a pull request. Before you do, once again make sure +that you have followed all the guidelines outlined in this document regarding code style, tests, and +documentation. You should also double check your branch changes against the branch it was based on by: + +1. Navigating to your repository on GitHub +1. Click on Branches +1. Click on the Compare button for your feature branch +1. Select the base and compare branches, if necessary. This will be develop and my-new-feature, respectively. + +### Make the pull request + +If everything looks good, you are ready to make a pull request. This is how you let the maintainers +of the cargo-leet project know you have code ready to be reviewed. To submit the pull request: + +1. Navigate to your repository on GitHub +1. Click on the Pull Request button for your feature branch +1. You can then click on Commits and Files Changed to make sure everything looks okay one last time +1. Write a description of your changes in the Conversation tab +1. Click Send Pull Request + +This request then goes to the repository maintainers, and they will review the code. + +### Updating your pull request + +Changes to your code may be needed based on the review of your pull request. +If this is the case you can make them in your branch, add a new commit to that branch, push it to GitHub, and the pull request will be automatically updated. +Pushing them to GitHub again is done by: + +```sh +git push origin my-new-feature +``` + +This will automatically update your pull request with the latest code and restart the Continuous Integration tests. + +Another reason you might need to update your pull request is to solve conflicts with changes that have been merged into the develop branch since you opened your pull request. + +To do this, you need to rebase your branch: + +```sh +git checkout my-new-feature +git fetch upstream +git rebase upstream/develop +``` + +There may be some merge conflicts that need to be resolved. +After the feature branch has been update locally, you can now update your pull request by pushing to the branch on GitHub: + +```sh +git push origin my-new-feature +``` + +If you rebased and get an error when pushing your changes you can resolve it with: + +```sh +git push origin my-new-feature --force +``` + +## Delete your merged branch (optional) + +Once your feature branch is accepted into upstream, you’ll probably want to get rid of the branch. +First, merge upstream develop into your develop branch so git knows it is safe to delete your branch: + +```sh +git fetch upstream +git checkout develop +git merge upstream/develop +``` + +Then you can do: + +```sh +git branch -d my-new-feature +``` + +Make sure you use a lower-case -d, or else git won’t warn you if your feature branch has not actually been merged. + +The branch will still exist on GitHub, so to delete it there do: + +```sh +git push origin --delete my-new-feature +``` diff --git a/Cargo.lock b/Cargo.lock index a17db06..5350977 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,12 +8,82 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aho-corasick" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" + +[[package]] +name = "anstyle-parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" + [[package]] name = "base64" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bumpalo" version = "3.13.0" @@ -22,11 +92,17 @@ checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" [[package]] name = "cargo-leet" -version = "0.1.0" +version = "0.2.0" dependencies = [ + "anyhow", + "clap", "convert_case", + "env_logger", + "log", + "regex", "serde", "serde_flat_path", + "strum", "ureq", ] @@ -42,6 +118,55 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "4.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8f255e4b8027970e78db75e78831229c9815fdbfa67eb1a1b777a62e24b4a0" +dependencies = [ + "clap_builder", + "clap_derive", + "once_cell", +] + +[[package]] +name = "clap_builder" +version = "4.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acd4f3c17c83b0ba34ffbc4f8bbd74f079413f747f84a6f89292f138057e36ab" +dependencies = [ + "anstream", + "anstyle", + "bitflags", + "clap_lex", + "once_cell", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "clap_lex" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "convert_case" version = "0.6.0" @@ -60,6 +185,40 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "flate2" version = "1.0.26" @@ -79,6 +238,24 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "idna" version = "0.4.0" @@ -89,6 +266,29 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] + +[[package]] +name = "is-terminal" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" +dependencies = [ + "hermit-abi", + "io-lifetimes", + "rustix", + "windows-sys", +] + [[package]] name = "itoa" version = "1.0.6" @@ -110,11 +310,23 @@ version = "0.2.146" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b" +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + [[package]] name = "log" -version = "0.4.18" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" + +[[package]] +name = "memchr" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "miniz_oxide" @@ -155,6 +367,23 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "regex" +version = "1.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" + [[package]] name = "ring" version = "0.16.20" @@ -170,6 +399,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "rustix" +version = "0.37.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "rustls" version = "0.20.8" @@ -182,6 +425,12 @@ dependencies = [ "webpki", ] +[[package]] +name = "rustversion" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" + [[package]] name = "ryu" version = "1.0.13" @@ -246,6 +495,34 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9f3bd7d2e45dcc5e265fbb88d6513e4747d8ef9444cf01a533119bce28a157" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.18", +] + [[package]] name = "syn" version = "1.0.109" @@ -268,6 +545,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -345,6 +631,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "wasm-bindgen" version = "0.2.86" @@ -444,8 +736,83 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" diff --git a/Cargo.toml b/Cargo.toml index e0e8a12..e33bfee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,43 @@ [package] name = "cargo-leet" -version = "0.1.0" +description = "Utility program to help with working on leetcode locally" +repository = "https://github.com/rust-practice/cargo-leet" +version = "0.2.0" +authors = ["Members of Rust Practice Discord Server"] +readme = "README.md" +license = "MIT OR Apache-2.0" edition = "2021" [dependencies] -convert_case = "0.6" -ureq = { version = "2.6", features = ["json"]} -serde = { version = "1", features = ["derive"] } -serde_flat_path = "0.1.2" +anyhow = { version = "1.0.71", optional = true } +convert_case = { version = "0.6", optional = true } +env_logger = { version = "0.10.0", optional = true } +log = { version = "0.4.18", optional = true } +regex = { version = "1.8.4", optional = true } +serde_flat_path = { version = "0.1.2", optional = true } +clap = { version = "4.3.3", features = ["derive", "cargo"], optional = true } +serde = { version = "1.0.164", features = ["derive"], optional = true } +ureq = { version = "2.6", features = ["json"], optional = true } +strum = { version = "0.25", features = ["derive"] } + +[[bin]] +name = "cargo-leet" +path = "src/main.rs" +required-features = ["tool"] + +[features] +default = ["leet_env"] +# Add support for leetcode's environment +leet_env = [] +# Items used when running as a binary +tool = [ + "anyhow", + "convert_case", + "env_logger", + "log", + "regex", + "serde_flat_path", + "clap", + "serde", + "ureq", +] diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..4fe2d18 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,200 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..9cf1062 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,19 @@ +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d6980e4 --- /dev/null +++ b/README.md @@ -0,0 +1,90 @@ +## cargo-leet - A leetcode local development assistant + +A program that given the link or slug to a leetcode problem, +creates a local file where you can develop and test your solution before post it back to leetcode. + +## ScreenShots + +### `cargo leet` + +![ScreenShot](assets/help_scr_shot_top.png) + +### `cargo leet generate --help` + +![ScreenShot](assets/help_scr_shot_generate.png) + +## Using Library Support + +Using the library to "mimic" leetcode environment. Add library as a dependency as below. Then add use statements as necessary. The use statements are automatically added if tool is used to generate the file for the problem. + +```toml +cargo-leet = { git = "https://github.com/rust-practice/cargo-leet.git", branch = "develop" } +``` + +## Tool Installation + +NB: If cargo-leet is already installed you do the install it will just replace it even it it was previously installed from a different source. For example if you install it from a clone then run the command to install from git it will replace the existing version that is installed (they will not both be installed). + +### From GitHub + +```sh +cargo install --git https://github.com/rust-practice/cargo-leet.git --branch main --features=tool +``` + +### From Clone + +After cloning the repo run + +```sh +cargo install --path . --features=tool +``` + +or using alias from `.cargo/config.toml` + +```sh +cargo i +``` + +## Running Directly from source without install (When developing the tool) + +These commands allow you to run the tool directly from the source code without installation. +By default they will run the tool on the current working directory. +This means that it will run in the current project folder for cargo-leet. +This may be fine for testing but if you want to be able to actually run the code, +it might be more appropriate to pass the path parameter and specify the path to the repository you want to to run against. +Eg. `cargo g --path $TEST_REPO` +For more options see [generate help](#cargo-leet-generate---help) + +```sh +cargo run --features=tool -- leet gen +``` + +or using alias from `.cargo/config.toml` + +```sh +cargo g +``` + +## Tool Uninstallation + +```sh +cargo uninstall cargo-leet +``` + +## License + +All code in this repository is dual-licensed under either: + +- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://apache.org/licenses/LICENSE-2.0) +- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. +This means you can select the license you prefer! +This dual-licensing approach is the de-facto standard in the Rust ecosystem and there are very good reasons to include both as noted in +this [issue](https://github.com/bevyengine/bevy/issues/2373) on [Bevy](https://bevyengine.org)'s repo. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you, as defined in the Apache-2.0 license, shall +be dual licensed as above, without any additional terms or conditions. diff --git a/assets/help_scr_shot_generate.png b/assets/help_scr_shot_generate.png new file mode 100644 index 0000000..a323e72 Binary files /dev/null and b/assets/help_scr_shot_generate.png differ diff --git a/assets/help_scr_shot_top.png b/assets/help_scr_shot_top.png new file mode 100644 index 0000000..776c45b Binary files /dev/null and b/assets/help_scr_shot_top.png differ diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..b9aa8ec --- /dev/null +++ b/deny.toml @@ -0,0 +1,8 @@ +# See defaults at https://github.com/EmbarkStudios/cargo-deny/blob/main/deny.template.toml +[advisories] +yanked = "deny" + +[licenses] +unlicensed = "warn" +allow = ["MIT", "Apache-2.0", "Apache-2.0 WITH LLVM-exception"] +default = "warn" diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..705974a --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,4 @@ +# `wrap_comments` is unstable so to use you need to use nightly. +# Uncomment the line below then run `cargo +nightly fmt` + +# wrap_comments = true diff --git a/src/code_snippet.rs b/src/code_snippet.rs deleted file mode 100644 index 41e972c..0000000 --- a/src/code_snippet.rs +++ /dev/null @@ -1,78 +0,0 @@ -use serde::Deserialize; -use serde_flat_path::flat_path; - -#[flat_path] -#[derive(Deserialize)] -struct CodeSnippetResponse { - #[flat_path("data.question.codeSnippets")] - code_snippets: Vec, -} -#[derive(Deserialize)] -struct CodeSnippet { - lang: String, - code: String, -} - -fn get_code_snippet_question(title_slug: &str) -> String { - let code_snippets_res = ureq::get("https://leetcode.com/graphql/") - .send_json(ureq::json!({ - "query": r#"query questionEditorData($titleSlug: String!) { - question(titleSlug: $titleSlug) { - codeSnippets { - lang - code - } - } - }"#, - "variables":{"titleSlug": title_slug}, - "operationName":"questionEditorData" - })) - .unwrap() - .into_json::() - .unwrap(); - code_snippets_res - .code_snippets - .into_iter() - .find_map(|cs| (cs.lang == "Rust").then_some(cs.code)) - .unwrap() -} - -fn get_test_cases(_title_slug: &str, is_design: bool) -> String { - let tests = if is_design { - r#" - use rstest::rstest; - "# - .to_string() - } else { - "".to_string() - }; - format!( - r#" - #[cfg(test)] - mod tests {{ - use super::*; - {tests} - }} - "# - ) -} - -pub fn generate_code_snippet(title_slug: &str) -> String { - // add URL - let mut code_snippet = format!("//! Solution for https://leetcode.com/problems/{title_slug}\n"); - - // get code snippet - let code = get_code_snippet_question(title_slug); - code_snippet.push_str(&code); - - // handle non design snippets - let is_design = code.starts_with("impl Solution {"); - if is_design { - code_snippet.push_str("\npub struct Solution;\n") - } - - // add tests - let test = get_test_cases(title_slug, is_design); - code_snippet.push_str(&test); - code_snippet -} diff --git a/src/leetcode_env/list.rs b/src/leetcode_env/list.rs index 9651d39..b24c266 100644 --- a/src/leetcode_env/list.rs +++ b/src/leetcode_env/list.rs @@ -2,9 +2,12 @@ use std::fmt::{Debug, Formatter}; +/// Definition for singly-linked list. #[derive(PartialEq, Eq)] pub struct ListNode { + /// The value stored at this node pub val: i32, + /// Links to the next node if it exists pub next: Option>, } @@ -24,6 +27,7 @@ impl Debug for ListNode { impl ListNode { #[inline] + /// Creates a new unlinked [ListNode] with the value passed pub fn new(val: i32) -> Self { ListNode { next: None, val } } @@ -56,18 +60,6 @@ impl From>> for ListHead { impl From> for ListHead { fn from(values: Vec) -> Self { - // Reverse version before looking at - // https://github.com/zwhitchcox/leetcode/blob/master/src/0002_add_two_numbers.rs - // to see how it could be done going forward instead of backward - // - // let mut last: Option> = None; - // for &n in values.iter().rev() { - // let mut temp = ListNode::new(n); - // temp.next = last; - // last = Some(Box::new(temp)); - // } - // ListHead::new(last) - let mut result = Self { head: None }; let mut curr = &mut result.head; for &num in &values { @@ -90,3 +82,45 @@ impl From<&ListHead> for Vec { result } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn from_vec_to_linked_list() { + // Arrange + let start_vec = vec![1, 2, 3, 4, 5]; + let expected = create_linked_list(1..=5); + + // Act + let list_head: ListHead = start_vec.into(); + let actual: Option> = list_head.into(); + + // Assert + assert_eq!(actual, expected); + } + + fn create_linked_list>(values: I) -> Option> { + let mut expected = None; + for i in values.rev() { + let mut new_node = Some(Box::new(ListNode::new(i))); + new_node.as_mut().unwrap().next = expected; + expected = new_node; + } + expected + } + + #[test] + fn from_linked_list_to_vec() { + // Arrange + let start: ListHead = create_linked_list(1..=5).into(); + let expected = vec![1, 2, 3, 4, 5]; + + // Act + let actual: Vec = (&start).into(); + + // Assert + assert_eq!(actual, expected); + } +} diff --git a/src/leetcode_env/mod.rs b/src/leetcode_env/mod.rs index d6c6485..cd4fea4 100644 --- a/src/leetcode_env/mod.rs +++ b/src/leetcode_env/mod.rs @@ -1,4 +1,6 @@ -//! Add support for "types" defined on leetcode and methods to facilitate conversion from example format +#![warn(missing_debug_implementations)] +//! Add support for "types" defined on leetcode and methods to facilitate +//! conversion from example format -pub mod list; -pub mod tree; +pub(crate) mod list; +pub(crate) mod tree; diff --git a/src/leetcode_env/tree.rs b/src/leetcode_env/tree.rs index 3a06be8..6e10052 100644 --- a/src/leetcode_env/tree.rs +++ b/src/leetcode_env/tree.rs @@ -7,15 +7,20 @@ use std::{ rc::Rc, }; +///Definition for a binary tree node. #[derive(PartialEq, Eq)] pub struct TreeNode { + /// The value stored at this node pub val: i32, + /// Link to the left child if one exists pub left: Option>>, + /// Link to the right child if one exists pub right: Option>>, } impl TreeNode { #[inline] + /// Creates a new [TreeNode] with no children and the value passed pub fn new(val: i32) -> Self { TreeNode { val, @@ -29,9 +34,11 @@ impl TreeNode { } } -// Wrapper class to make handling empty trees easier +/// Wrapper class to make handling empty trees easier and building of trees +/// easier via [From] impls #[derive(PartialEq, Eq)] pub struct TreeRoot { + /// The root of the tree held pub root: Option>>, } @@ -79,6 +86,15 @@ impl From<&TreeRoot> for Vec> { } } } + + // Trim trailing None + while let Some(_last) = result.last() { + if _last.is_none() { + result.pop(); + } else { + break; + } + } result } } @@ -90,8 +106,8 @@ impl From>>> for TreeRoot { } impl From<&str> for TreeRoot { - /// Expects the "[]" around the values, separated by comma "," and only integers and "null" - /// (which is the format you'll get on LeetCode) + /// Expects the "[]" around the values, separated by comma "," and only + /// integers and "null" (which is the format you'll get on LeetCode) /// /// # Panics /// @@ -191,3 +207,80 @@ impl From for Option>> { value.root } } + +#[cfg(test)] +mod tests { + use super::*; + + /// Creates the test tree seen below + /// Leetcode rep: [1,2,5,3,null,6,7,null,4,null,null,8] + /// 1 + /// / \ + /// / \ + /// / \ + /// 2 5 + /// / \ / \ + /// 3 - 6 7 + /// / \ / \ / \ + /// - 4 - - 8 - + #[allow(unused_mut)] // It's easier to read the code if they all line up but the leaves don't need + // to be mutable + fn test_tree() -> Option>> { + let mut node1 = Some(Rc::new(RefCell::new(TreeNode::new(1)))); + let mut node2 = Some(Rc::new(RefCell::new(TreeNode::new(2)))); + let mut node3 = Some(Rc::new(RefCell::new(TreeNode::new(3)))); + let mut node4 = Some(Rc::new(RefCell::new(TreeNode::new(4)))); + let mut node5 = Some(Rc::new(RefCell::new(TreeNode::new(5)))); + let mut node6 = Some(Rc::new(RefCell::new(TreeNode::new(6)))); + let mut node7 = Some(Rc::new(RefCell::new(TreeNode::new(7)))); + let mut node8 = Some(Rc::new(RefCell::new(TreeNode::new(8)))); + node3.as_mut().unwrap().borrow_mut().right = node4; + node7.as_mut().unwrap().borrow_mut().left = node8; + node2.as_mut().unwrap().borrow_mut().left = node3; + node5.as_mut().unwrap().borrow_mut().left = node6; + node5.as_mut().unwrap().borrow_mut().right = node7; + node1.as_mut().unwrap().borrow_mut().left = node2; + node1.as_mut().unwrap().borrow_mut().right = node5; + node1 + } + + #[test] + fn from_tree_to_vec() { + // Arrange + let start: TreeRoot = test_tree().into(); + let expected = vec![ + Some(1), + Some(2), + Some(5), + Some(3), + None, + Some(6), + Some(7), + None, + Some(4), + None, + None, + Some(8), + ]; + + // Act + let actual: Vec> = (&start).into(); + + // Assert + assert_eq!(actual, expected); + } + + #[test] + fn from_str_to_tree() { + // Arrange + let start = "[1,2,5,3,null,6,7,null,4,null,null,8]"; + let expected = test_tree(); + + // Act + let root: TreeRoot = start.into(); + let actual: Option>> = root.into(); + + // Assert + assert_eq!(actual, expected); + } +} diff --git a/src/lib.rs b/src/lib.rs index 3f80424..69522f4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,58 @@ +#![forbid(unsafe_code)] +#![warn(missing_docs)] +#![warn(rustdoc::missing_crate_level_docs)] +#![warn(unreachable_pub)] + +//! The main aim of **cargo-leet** is to make it easier to develop solutions to +//! leetcode problems locally on your machine. And as such it is composed of two +//! parts, a tool to help with code download and testing setup, and helper code +//! to allow running solutions developed locally. +//! +//! ### Tool +//! +//! The **cargo-leet** subcommand is a command line tool developed with clap and +//! the associated help is probably the best way to get an idea of how to use +//! the tool. Screenshots of the help can be found in the +//! [readme](https://github.com/rust-practice/cargo-leet#screenshots) on github. +//! For the sake of maintainability features added will be documented there +//! instead of always needing to update multiple places. +//! +//! ### Leetcode Environment Support +//! +//! **cargo-leet** also includes helper code with structs and traits to simulate +//! the environment that your code would run in on the leetcode servers so that +//! you are able to run tests on your code locally. It also provides a few extra +//! types that facilitate testing especially as it relates to creating test +//! cases from the text provided by leetcode. +//! +//! ## Feature flags +//! +//! **cargo-leet** uses feature flags to control which code gets compiled based +//! on how the crate is being used. This is especially important for the code +//! imported in the solution repository as this repo may be using a much older +//! version of the rust toolchain due to the fact that leetcode uses a much +//! older version on their servers and some users may want to use the same +//! version to ensure their code will always work upon upload. However, because +//! it is such an old version many of the crates used in the development of the +//! tool are not able to be compiled with that toolchain and as such they being +//! only compiled behind a feature flag makes that a non-issue. It also allows +//! users to not compile the code only needed to support leetcode solution +//! development when working on or using the tool. +//! +//! - `default`: Enables the `leet_env` feature as this is the most common use +//! case +//! - `leet_env`: Includes the code for working on leetcode problem solutions +//! - `tool`: Enables the code and dependencies used to create the tool. + +#[cfg(feature = "leet_env")] mod leetcode_env; +#[cfg(feature = "leet_env")] +pub use leetcode_env::{ + list::{ListHead, ListNode}, + tree::{TreeNode, TreeRoot}, +}; -pub use leetcode_env::list::ListHead; -pub use leetcode_env::list::ListNode; -pub use leetcode_env::tree::TreeNode; -pub use leetcode_env::tree::TreeRoot; +#[cfg(feature = "tool")] +mod tool; +#[cfg(feature = "tool")] +pub use crate::tool::{cli::CargoCli, core::run, log::init_logging}; diff --git a/src/main.rs b/src/main.rs index bf6e90b..8a3ee25 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,8 @@ -mod code_snippet; -mod daily_challenge; -mod write_file; +use cargo_leet::{init_logging, run, CargoCli}; +use clap::Parser; -use std::error::Error; - -fn main() -> Result<(), Box> { - let mut args = std::env::args(); - let title_slug = if args.len() == 1 { - daily_challenge::get_daily_challenge_slug() - } else if args.len() != 2 { - return Err("Usage: binary SLUG".into()); - } else { - args.nth(1).unwrap() - }; - let code_snippet = code_snippet::generate_code_snippet(&title_slug); - write_file::write_file(&title_slug, code_snippet)?; - Ok(()) +fn main() -> anyhow::Result<()> { + let CargoCli::Leet(cli) = CargoCli::parse(); + init_logging(cli.log_level.into())?; + run(&cli) } diff --git a/src/tool/cli.rs b/src/tool/cli.rs new file mode 100644 index 0000000..e1d0267 --- /dev/null +++ b/src/tool/cli.rs @@ -0,0 +1,108 @@ +use std::env; + +use anyhow::Context; +use clap::{Args, Parser, Subcommand, ValueEnum}; +use log::{debug, info, LevelFilter}; + +/// Top level entry point for command line arguments parsing +/// +/// Based on example +#[derive(Parser)] +#[command(name = "cargo")] +#[command(bin_name = "cargo")] +pub enum CargoCli { + /// This is necessary because it a cargo subcommand so the first argument + /// needs to be the command name + Leet(Cli), +} +#[derive(Args, Debug)] +#[command(author, version, about, long_about = None)] +pub struct Cli { + #[command(subcommand)] + pub command: Commands, + + /// Specify the path to the project root (If not provided uses current + /// working directory) + #[arg(long, short, global = true, value_name = "FOLDER")] + path: Option, + + /// Set logging level to use + #[arg(long, short, global = true, value_enum, default_value_t = LogLevel::Warn)] + pub log_level: LogLevel, +} + +impl Cli { + /// Changes the current working directory to path if one is given + pub fn update_current_working_dir(&self) -> anyhow::Result<()> { + debug!( + "Before attempting update current dir, it is: {}", + env::current_dir()?.display() + ); + if let Some(path) = &self.path { + info!("Going to update working directory to to '{path}'"); + std::env::set_current_dir(path) + .with_context(|| format!("Failed to set current dir to: '{path}'"))?; + info!( + "After updating current dir, it is: '{}'", + env::current_dir()?.display() + ); + } else { + debug!("No user supplied path found. No change") + } + Ok(()) + } +} + +#[derive(Subcommand, Debug)] +pub enum Commands { + #[clap(visible_alias = "gen", short_flag = 'g')] + Generate(GenerateArgs), +} + +#[derive(Args, Debug)] +pub struct GenerateArgs { + /// Question slug or url (If none specified then daily challenge is used) + pub problem: Option, + /// If set the module name generated includes the number for the problem + #[arg(short = 'n', long = "number_in_name", default_value_t = false)] + pub should_include_problem_number: bool, +} + +/// Exists to provide better help messages variants copied from LevelFilter as +/// that's the type that is actually needed +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)] +pub enum LogLevel { + /// Nothing emitted in this mode + Off, + Error, + Warn, + Info, + Debug, + Trace, +} + +impl From for LevelFilter { + fn from(value: LogLevel) -> Self { + match value { + LogLevel::Off => LevelFilter::Off, + LogLevel::Error => LevelFilter::Error, + LogLevel::Warn => LevelFilter::Warn, + LogLevel::Info => LevelFilter::Info, + LogLevel::Debug => LevelFilter::Debug, + LogLevel::Trace => LevelFilter::Trace, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn verify_cli() { + // Source: https://docs.rs/clap/latest/clap/_derive/_tutorial/index.html#testing + // My understanding it reports most development errors without additional effort + use clap::CommandFactory; + CargoCli::command().debug_assert() + } +} diff --git a/src/tool/config.rs b/src/tool/config.rs new file mode 100644 index 0000000..52a600d --- /dev/null +++ b/src/tool/config.rs @@ -0,0 +1,23 @@ +pub(crate) struct Config {} + +impl Config { + // assumed in the code using them URLs Must end with trailing "/" + pub(crate) const LEETCODE_PROBLEM_URL: &'static str = "https://leetcode.com/problems/"; + pub(crate) const LEETCODE_GRAPH_QL: &'static str = "https://leetcode.com/graphql/"; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn problem_url_ends_with_slash() { + // TODO: Switch to using https://docs.rs/static_assertions/latest/static_assertions/ + assert!(Config::LEETCODE_PROBLEM_URL.ends_with('/')); + } + + #[test] + fn graph_ql_url_ends_with_slash() { + assert!(Config::LEETCODE_GRAPH_QL.ends_with('/')); + } +} diff --git a/src/tool/core/generate.rs b/src/tool/core/generate.rs new file mode 100644 index 0000000..433032a --- /dev/null +++ b/src/tool/core/generate.rs @@ -0,0 +1,167 @@ +use anyhow::{bail, Context}; +use convert_case::{Case, Casing}; +use log::info; +use std::borrow::Cow; + +use crate::tool::{ + cli, + config::Config, + core::helpers::{ + code_snippet::get_code_snippet_for_problem, daily_challenge, + problem_metadata::get_problem_metadata, write_to_disk, + }, +}; + +pub(crate) fn do_generate(args: &cli::GenerateArgs) -> anyhow::Result<()> { + let title_slug: Cow = if let Some(specific_problem) = &args.problem { + get_slug_from_args(specific_problem) + .with_context(|| format!("Expected URL or slug but got {specific_problem}"))? + } else { + // Daily problem + let slug = daily_challenge::get_daily_challenge_slug()?; + info!("Slug for daily problem is: '{slug}'"); + Cow::Owned(slug) + }; + + let (module_name, module_code) = create_module_code(title_slug, args) + .context("Failed to generate the name and module code")?; + write_to_disk::write_file(&module_name, module_code).context("Failed to write to disk")?; + println!("Generated module: {module_name}"); + Ok(()) +} + +fn get_slug_from_args(specific_problem: &String) -> anyhow::Result> { + Ok(if is_url(specific_problem) { + // Working with a url + info!("Using '{specific_problem}' as a url"); + let slug = url_to_slug(specific_problem)?; + info!("Extracted slug '{slug}' from url"); + Cow::Owned(slug) + } else { + // This is expected to be a valid slug + info!("Using '{specific_problem}' as a slug"); + Cow::Borrowed(specific_problem) + }) +} + +/// Gets the code and other data from leetcode and generates the suitable code +/// for the module and the name of the module Returns the module name and the +/// module code +/// +/// NB: Did not return `Cow` because `module_name` is always a modified version +/// of the input +fn create_module_code( + title_slug: Cow, + args: &cli::GenerateArgs, +) -> anyhow::Result<(String, String)> { + info!("Building module contents for {title_slug}"); + + let meta_data = + get_problem_metadata(&title_slug).context("Failed to retrieve problem meta data")?; + + // Add problem URL + let mut code_snippet = format!( + "//! Solution for {}{title_slug}\n", + Config::LEETCODE_PROBLEM_URL + ); + + // Add problem number and title + code_snippet.push_str(&format!( + "//! {}\n", + meta_data + .get_num_and_title() + .context("Failed to get problem number and title")? + )); + + // Add blank line between docstring and code + code_snippet.push('\n'); + + // Get code snippet + let problem_code = get_code_snippet_for_problem(&title_slug)?; + code_snippet.push_str(problem_code.as_ref()); + + code_snippet.push_str( + "\n\n// << ---------------- Code below here is only for local use ---------------- >>\n", + ); + + // Add struct for non design questions + if problem_code.type_.is_non_design() { + code_snippet.push_str("\npub struct Solution;\n") + } + + // Add leet code types + if problem_code.has_tree() { + code_snippet.push_str("use cargo_leet::TreeNode;\n"); + } + if problem_code.has_list() { + code_snippet.push_str("use cargo_leet::ListNode;\n"); + } + + // Add tests + let tests = meta_data.get_test_cases(&problem_code)?; + code_snippet.push_str(&tests); + + // Set module name + let module_name = if args.should_include_problem_number { + info!("Including problem number in module name"); + format!( + "_{}_{}", + meta_data.get_id()?, + title_slug.to_case(Case::Snake) + ) + } else { + info!("Using snake case slug for module name"); + title_slug.to_case(Case::Snake) + }; + + Ok((module_name, code_snippet)) +} + +/// Quick and dirty test to see if this is a url +/// Uses a character that is not allowed in slugs but must be in a url to decide +/// between the two +fn is_url(value: &str) -> bool { + value.contains('/') +} + +fn url_to_slug(url: &str) -> anyhow::Result { + debug_assert!(Config::LEETCODE_PROBLEM_URL.ends_with('/')); + if !url.starts_with(Config::LEETCODE_PROBLEM_URL) { + bail!( + "Expected a leetcode url that starts with '{}' but got '{url}'", + Config::LEETCODE_PROBLEM_URL + ) + } + let split_url: Vec<_> = url.split('/').collect(); + let split_prefix: Vec<_> = Config::LEETCODE_PROBLEM_URL.split('/').collect(); + debug_assert!(split_prefix.len() < split_url.len()); + Ok(split_url[split_prefix.len() - 1].to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn slug_in_slug_out() { + let slug = "two-sum".to_string(); + let actual = get_slug_from_args(&slug).expect("Expect value to be valid"); + assert_eq!(actual.to_string(), slug); + } + + #[test] + fn url_in_slug_out() { + let url = "https://leetcode.com/problems/two-sum/".to_string(); + let expected = "two-sum"; + let actual = get_slug_from_args(&url).expect("Expect value to be valid"); + assert_eq!(actual.to_string(), expected); + } + + #[test] + fn invalid_url() { + // Missing "s" in https + let url = "http://leetcode.com/problems/two-sum/".to_string(); + let actual = get_slug_from_args(&url); + assert!(actual.is_err()); + } +} diff --git a/src/tool/core/helpers/code_snippet.rs b/src/tool/core/helpers/code_snippet.rs new file mode 100644 index 0000000..9cafdd6 --- /dev/null +++ b/src/tool/core/helpers/code_snippet.rs @@ -0,0 +1,56 @@ +use super::problem_code::ProblemCode; +use crate::tool::config::Config; +use anyhow::{bail, Context}; +use log::info; +use regex::Regex; +use serde::Deserialize; +use serde_flat_path::flat_path; + +#[flat_path] +#[derive(Deserialize)] +struct CodeSnippetResponse { + #[flat_path("data.question.codeSnippets")] + code_snippets: Vec, +} +#[derive(Deserialize)] +struct CodeSnippet { + lang: String, + code: String, +} + +pub(crate) fn get_code_snippet_for_problem(title_slug: &str) -> anyhow::Result { + info!("Going to get code for {title_slug}"); + let code_snippets_res = ureq::get(Config::LEETCODE_GRAPH_QL) + .send_json(ureq::json!({ + "query": r#"query questionEditorData($titleSlug: String!) { + question(titleSlug: $titleSlug) { + codeSnippets { + lang + code + } + } + }"#, + "variables":{"titleSlug": title_slug}, + "operationName":"questionEditorData" + })) + .context("Get request for code_snippet failed")? + .into_json::() + .context("Failed to convert response from json to codes_snippet")?; + + let mut result = match code_snippets_res + .code_snippets + .into_iter() + .find_map(|cs| (cs.lang == "Rust").then_some(cs.code)) + { + Some(result) => result, + None => bail!("Rust not supported for this problem"), + }; + + // Add todo!() placeholders in function bodies + let re = Regex::new(r#"\{\s*\}"#)?; + result = re + .replace_all(&result, "{ todo!(\"Fill in body\") }") + .to_string(); + + result.try_into() +} diff --git a/src/daily_challenge.rs b/src/tool/core/helpers/daily_challenge.rs similarity index 60% rename from src/daily_challenge.rs rename to src/tool/core/helpers/daily_challenge.rs index c584f5f..6b67912 100644 --- a/src/daily_challenge.rs +++ b/src/tool/core/helpers/daily_challenge.rs @@ -1,6 +1,9 @@ +use anyhow::Context; use serde::Deserialize; use serde_flat_path::flat_path; +use crate::tool::config::Config; + #[flat_path] #[derive(Deserialize)] struct DailyChallengeResponse { @@ -8,21 +11,21 @@ struct DailyChallengeResponse { title_slug: String, } -pub fn get_daily_challenge_slug() -> String { - let daily_challenge_response = ureq::get("https://leetcode.com/graphql/") +pub(crate) fn get_daily_challenge_slug() -> anyhow::Result { + let daily_challenge_response = ureq::get(Config::LEETCODE_GRAPH_QL) .send_json(ureq::json!({ "query": r#"query questionOfToday { activeDailyCodingChallengeQuestion { question { titleSlug } - } + } }"#, "variables":{}, "operationName":"questionOfToday" })) - .unwrap() + .context("Get request for daily challenge failed")? .into_json::() - .unwrap(); - daily_challenge_response.title_slug + .context("Failed to convert response for daily challenge from json")?; + Ok(daily_challenge_response.title_slug) } diff --git a/src/tool/core/helpers/mod.rs b/src/tool/core/helpers/mod.rs new file mode 100644 index 0000000..5a6f015 --- /dev/null +++ b/src/tool/core/helpers/mod.rs @@ -0,0 +1,5 @@ +pub(crate) mod code_snippet; +pub(crate) mod daily_challenge; +pub(crate) mod problem_code; +pub(crate) mod problem_metadata; +pub(crate) mod write_to_disk; diff --git a/src/tool/core/helpers/problem_code.rs b/src/tool/core/helpers/problem_code.rs new file mode 100644 index 0000000..4e75775 --- /dev/null +++ b/src/tool/core/helpers/problem_code.rs @@ -0,0 +1,793 @@ +use std::fmt::Display; + +use anyhow::{bail, Context}; +use log::{debug, info, warn}; +use regex::Regex; + +#[derive(Debug)] +pub(crate) struct ProblemCode { + code: String, + pub(crate) type_: ProblemType, +} + +#[derive(Debug)] +pub(crate) enum ProblemType { + NonDesign(FunctionInfo), + Design, +} + +impl ProblemType { + /// Returns `true` if the problem type is [`NonDesign`]. + /// + /// [`NonDesign`]: ProblemType::NonDesign + #[must_use] + pub(crate) fn is_non_design(&self) -> bool { + matches!(self, Self::NonDesign(..)) + } +} + +impl TryFrom for ProblemCode { + type Error = anyhow::Error; + + fn try_from(code: String) -> Result { + let type_ = if Self::is_design(&code) { + info!("Problem Type is Design"); + ProblemType::Design + } else { + info!("Problem Type is NonDesign"); + ProblemType::NonDesign(Self::get_fn_info(&code).context("Failed to get function info")?) + }; + let result = Self { code, type_ }; + debug!("ProblemCode built: {result:#?}"); + Ok(result) + } +} + +impl AsRef for ProblemCode { + fn as_ref(&self) -> &str { + &self.code + } +} + +impl ProblemCode { + fn is_design(code: &str) -> bool { + !code.contains("impl Solution {") + } + + fn get_fn_info(code: &str) -> anyhow::Result { + let re = Regex::new(r#"(?s)\n\s*pub fn ([a-z_0-9]*)\s*\((.*)\)(?: ?-> ?(.*))? \{"#)?; + let caps = if let Some(caps) = re.captures(code) { + caps + } else { + bail!("Regex failed to match"); + }; + + let name = if let Some(name) = caps.get(1) { + name.as_str().to_string() + } else { + bail!("Function name not found in code") + }; + + let args = FunctionArgs::new(if let Some(args) = caps.get(2) { + args.as_str().to_string() + } else { + bail!("Function arguments not matched") + }) + .context("Failed to parse function arguments")?; + + let return_type: Option = match caps.get(3) { + Some(s) => Some( + s.as_str() + .try_into() + .context("Failed to convert return type")?, + ), + None => None, + }; + + Ok(FunctionInfo { + name, + fn_args: args, + return_type, + }) + } + + pub(crate) fn has_tree(&self) -> bool { + if let ProblemType::NonDesign(fn_info) = &self.type_ { + fn_info.has_tree() + } else { + false + } + } + + pub(crate) fn has_list(&self) -> bool { + if let ProblemType::NonDesign(fn_info) = &self.type_ { + fn_info.has_list() + } else { + false + } + } +} + +#[derive(Debug)] +pub(crate) struct FunctionInfo { + pub(crate) name: String, + fn_args: FunctionArgs, + return_type: Option, +} + +impl FunctionInfo { + pub(crate) fn get_args_with_case(&self) -> String { + let mut result = String::from("#[case] "); + result.push_str(&self.fn_args.raw_str.replace(',', ", #[case] ")); + + if let Some(return_type) = self.return_type.as_ref() { + result.push_str(&format!(", #[case] expected: {return_type}")) + } + result + } + + pub(crate) fn get_args_names(&self) -> String { + let names: Vec<_> = self + .fn_args + .args + .iter() + .map(|arg| arg.identifier.clone()) + .collect(); + names.join(", ") + } + + pub(crate) fn get_solution_comparison_code(&self) -> String { + if let Some(FunctionArgType::F64) = &self.return_type { + "assert!((actual - expected).abs() < 1e-5, \"Assertion failed: actual {actual:.5} but expected {expected:.5}. Diff is more than 1e-5.\");" + } else { + "assert_eq!(actual, expected);" + } + .to_string() + } + + pub(crate) fn get_test_case(&self, example_test_case_raw: &str) -> anyhow::Result { + let mut result = String::new(); + let n = self.fn_args.len(); + let lines: Vec<_> = example_test_case_raw.lines().collect(); + + if lines.len() != self.fn_args.len() { + bail!( + "Expected number of augments ({}) to match the number of lines download ({})", + self.fn_args.len(), + lines.len() + ) + } + + for (i, (&line, arg_type)) in lines + .iter() + .zip(self.fn_args.args.iter().map(|arg| &arg.arg_type)) + .enumerate() + { + result.push_str( + &arg_type + .apply(line) + .context("Failed to apply type information to the example from leetcode")?, + ); + + if i < n - 1 { + result.push_str(", "); + } + } + + // Include return type + if self.return_type.is_some() { + result.push_str(", todo!(\"Expected Result\")"); + } + + Ok(result) + } + + fn has_tree(&self) -> bool { + self.fn_args.args.iter().any(|arg| arg.arg_type.is_tree()) + } + + fn has_list(&self) -> bool { + self.fn_args.args.iter().any(|arg| arg.arg_type.is_list()) + } +} + +#[derive(Debug)] +pub(crate) struct FunctionArg { + identifier: String, + arg_type: FunctionArgType, +} + +#[derive(Debug)] +struct FunctionArgs { + raw_str: String, + args: Vec, +} + +impl FunctionArgs { + fn new(raw_str: String) -> anyhow::Result { + let re = Regex::new(r#"([A-Za-z_0-9]+?)\s*:\s*([A-Za-z0-9<>]*)"#)?; + let caps: Vec<_> = re.captures_iter(&raw_str).collect(); + let mut args: Vec = vec![]; + for cap in caps { + let identifier = cap.get(1).expect("Required to match").as_str().to_string(); + let arg_type = cap + .get(2) + .expect("Required to match") + .as_str() + .try_into() + .context("Failed to get argument type")?; + + args.push(FunctionArg { + identifier, + arg_type, + }) + } + + Ok(Self { raw_str, args }) + } + + fn len(&self) -> usize { + self.args.len() + } +} + +/// Function Arg Type (FAT) +#[cfg_attr(debug_assertions, derive(strum::EnumIter))] +#[derive(Debug, Eq, Hash, PartialEq)] +enum FunctionArgType { + I32, + I64, + F64, + Bool, + String_, + VecI32, + VecF64, + VecBool, + VecString, + VecVecI32, + VecVecString, + List, + Tree, + Other { raw: String }, +} + +impl FunctionArgType { + /// Applies any special changes needed to the value based on the type + fn apply(&self, line: &str) -> anyhow::Result { + debug!("Going to apply changes to argument input for {self:#?} to {line:?}"); + use FunctionArgType::*; + let result = match self { + String_ | Bool => Ok(line.to_string()), + I32 => match line.parse::() { + Ok(_) => Ok(line.to_string()), + Err(e) => Err(format!( + "In testing the test input {line:?} the parsing to i32 failed with error: {e}" + )), + }, + I64 => match line.parse::() { + Ok(_) => Ok(line.to_string()), + Err(e) => Err(format!( + "In testing the test input {line:?} the parsing to i64 failed with error: {e}" + )), + }, + F64 => match line.parse::() { + Ok(_) => Ok(line.to_string()), + Err(e) => Err(format!( + "In testing the test input {line:?} the parsing to f64 failed with error: {e}" + )), + }, + VecI32 | VecBool | VecF64 | VecVecI32 | VecString | VecVecString => { + match Self::does_pass_basic_vec_tests(line) { + Ok(_) => { + let mut result = line.to_string(); + if [VecString, VecVecString].contains(self) { + result = result.replace("\",", "\".into(),"); // Replace ones before end + result = result.replace("\"]", "\".into()]"); // Replace end + } + Ok(result.replace('[', "vec![")) + } + Err(e) => Err(e.to_string()), + } + } + List => match Self::does_pass_basic_vec_tests(line) { + Ok(_) => Ok(format!("ListHead::from(vec!{line}).into()")), + Err(e) => Err(e.to_string()), + }, + Tree => match Self::does_pass_basic_vec_tests(line) { + Ok(_) => Ok(format!("TreeRoot::from(\"{line}\").into()")), + Err(e) => Err(e.to_string()), + }, + Other { raw: _ } => Ok(format!("todo!(\"{line}\")")), + }; + match result { + Ok(result) => Ok(result), + Err(e) => { + warn!("Type Mismatch? Type detected as '{self:?}' but got argument value of {line:?}. Error: {e}"); + Ok(format!("todo!({line:?})")) + } + } + } + + fn does_pass_basic_vec_tests(s: &str) -> anyhow::Result<()> { + if !s.starts_with('[') || !s.ends_with(']') { + bail!("Expecting something that can be represented as a vec but got {s:?}"); + } + Ok(()) + } + + fn is_tree(&self) -> bool { + matches!(self, FunctionArgType::Tree) + } + + fn is_list(&self) -> bool { + matches!(self, FunctionArgType::List) + } +} + +impl Display for FunctionArgType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use FunctionArgType::*; + let s = match self { + I32 => "i32", + I64 => "i64", + F64 => "f64", + Bool => "bool", + String_ => "String", + VecI32 => "Vec", + VecF64 => "Vec", + VecBool => "Vec", + VecString => "Vec", + VecVecI32 => "Vec>", + VecVecString => "Vec>", + List => "Option>", + Tree => "Option>>", + Other { raw } => raw, + }; + + write!(f, "{s}") + } +} + +impl TryFrom<&str> for FunctionArgType { + type Error = anyhow::Error; + + fn try_from(value: &str) -> Result { + use FunctionArgType::*; + Ok(match value.trim() { + "i32" => I32, + "i64" => I64, + "f64" => F64, + "bool" => Bool, + "String" => String_, + "Vec" => VecI32, + "Vec" => VecF64, + "Vec" => VecBool, + "Vec" => VecString, + "Vec>" => VecVecI32, + "Vec>" => VecVecString, + "Option>" => List, + "Option>>" => Tree, + trimmed_value => { + warn!("Unknown type {trimmed_value:?} found please report this in an issue https://github.com/rust-practice/cargo-leet/issues/new?&labels=bug&template=missing_type.md"); + Other { + raw: trimmed_value.to_string(), + } + } + }) + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashSet; + + use strum::IntoEnumIterator; + + use super::*; + + fn get_100_same_tree() -> &'static str { + "// Definition for a binary tree node. +// #[derive(Debug, PartialEq, Eq)] +// pub struct TreeNode { +// pub val: i32, +// pub left: Option>>, +// pub right: Option>>, +// } +// +// impl TreeNode { +// #[inline] +// pub fn new(val: i32) -> Self { +// TreeNode { +// val, +// left: None, +// right: None +// } +// } +// } +use std::rc::Rc; +use std::cell::RefCell; +impl Solution { + pub fn is_same_tree(p: Option>>, q: Option>>) -> bool { + + } +} +" + } + + fn get_97_interleaving_string() -> &'static str { + "impl Solution { + pub fn is_interleave(s1: String, s2: String, s3: String) -> bool { + + } +} +" + } + + fn get_706_design_hashmap() -> &'static str { + "struct MyHashMap { + +} + + +/** + * `&self` means the method takes an immutable reference. + * If you need a mutable reference, change it to `&mut self` instead. + */ +impl MyHashMap { + + fn new() -> Self { + + } + + fn put(&self, key: i32, value: i32) { + + } + + fn get(&self, key: i32) -> i32 { + + } + + fn remove(&self, key: i32) { + + } +} + +/** + * Your MyHashMap object will be instantiated and called as such: + * let obj = MyHashMap::new(); + * obj.put(key, value); + * let ret_2: i32 = obj.get(key); + * obj.remove(key); + */ +" + } + + fn get_2_add_two_numbers() -> &'static str { + " +// Definition for singly-linked list. +// #[derive(PartialEq, Eq, Clone, Debug)] +// pub struct ListNode { +// pub val: i32, +// pub next: Option> +// } +// +// impl ListNode { +// #[inline] +// fn new(val: i32) -> Self { +// ListNode { +// next: None, +// val +// } +// } +// } +impl Solution { + pub fn add_two_numbers(l1: Option>, l2: Option>) -> Option> { + + } +} +" + } + + #[test] + fn has_tree_with_tree() { + // Arrange / Act + let problem_code: ProblemCode = get_100_same_tree() + .to_string() + .try_into() + .expect("Should be valid code"); + + // Assert + assert!(problem_code.has_tree()); + } + + #[test] + fn has_tree_without_tree() { + // Arrange / Act + let problem_code: ProblemCode = get_97_interleaving_string() + .to_string() + .try_into() + .expect("Should be valid code"); + + // Assert + assert!(!problem_code.has_tree()); + } + + #[test] + fn has_tree_design_question() { + // Arrange / Act + let problem_code: ProblemCode = get_706_design_hashmap() + .to_string() + .try_into() + .expect("Should be valid code"); + + // Assert + assert!(!problem_code.has_tree()); + } + + #[test] + fn has_list_with_list() { + // Arrange / Act + let problem_code: ProblemCode = get_2_add_two_numbers() + .to_string() + .try_into() + .expect("Should be valid code"); + + // Assert + assert!(problem_code.has_list()); + } + + #[test] + fn has_list_without_list() { + // Arrange / Act + let problem_code: ProblemCode = get_97_interleaving_string() + .to_string() + .try_into() + .expect("Should be valid code"); + + // Assert + assert!(!problem_code.has_list()); + } + + #[test] + fn has_list_design_question() { + // Arrange / Act + let problem_code: ProblemCode = get_706_design_hashmap() + .to_string() + .try_into() + .expect("Should be valid code"); + + // Assert + assert!(!problem_code.has_list()); + } + + #[test] + fn get_args_with_case() { + // Arrange / Act + let fn_info = extract_function_info(get_97_interleaving_string()); + + // Assert + assert_eq!( + fn_info.get_args_with_case(), + "#[case] s1: String, #[case] s2: String, #[case] s3: String, #[case] expected: bool" + ); + } + + #[test] + fn get_args_names() { + // Arrange / Act + let fn_info = extract_function_info(get_97_interleaving_string()); + + // Assert + assert_eq!(fn_info.get_args_names(), "s1, s2, s3"); + } + + fn get_fn_info_min_sub_array_len() -> FunctionInfo { + FunctionInfo { + name: "min_sub_array_len".into(), + fn_args: FunctionArgs { + raw_str: "target: i32, nums: Vec".into(), + args: vec![ + FunctionArg { + identifier: "target".into(), + arg_type: FunctionArgType::I32, + }, + FunctionArg { + identifier: "nums".into(), + arg_type: FunctionArgType::VecI32, + }, + ], + }, + return_type: Some(FunctionArgType::I32), + } + } + + #[test] + fn get_test_case_ok() { + // Arrange + let expected = "7, vec![2,3,1,2,4,3], todo!(\"Expected Result\")"; + let fn_info = get_fn_info_min_sub_array_len(); + let input = "7\n[2,3,1,2,4,3]"; + + // Act + let actual = fn_info.get_test_case(input).expect("Expected Ok"); + + // Assert + assert_eq!(actual, expected); + } + + #[test] + fn get_test_case_invalid_num_args() { + // Arrange + let fn_info = get_fn_info_min_sub_array_len(); + let input = "[2,3,1,2,4,3]"; + + // Act + let actual = fn_info.get_test_case(input); + + // Assert + assert!(actual.is_err()); + } + + fn create_code_stub_all_arg_types_non_design() -> &'static str { + " +impl Solution { + pub fn func_name( + L2AC6p: i32, + q7kv5k: i64, + pP7GhC: f64, + HFGzdD: bool, + kjACSr: String, + ePfFj3: Vec, + kRubF2: Vec, + ykyF5X: Vec, + NkCeR6: Vec, + bBtcWe: Vec>, + ndi4ny: Vec>, + bJy3HH: Option>, + ndQLTu: Option>>, + PRnJhw: UnknownType, + ) { + } +} +" + } + + fn fn_type_to_id(fat: &FunctionArgType) -> &'static str { + match fat { + FunctionArgType::I32 => "L2AC6p", + FunctionArgType::I64 => "q7kv5k", + FunctionArgType::F64 => "pP7GhC", + FunctionArgType::Bool => "HFGzdD", + FunctionArgType::String_ => "kjACSr", + FunctionArgType::VecI32 => "ePfFj3", + FunctionArgType::VecF64 => "kRubF2", + FunctionArgType::VecBool => "ykyF5X", + FunctionArgType::VecString => "NkCeR6", + FunctionArgType::VecVecI32 => "bBtcWe", + FunctionArgType::VecVecString => "ndi4ny", + FunctionArgType::List => "bJy3HH", + FunctionArgType::Tree => "ndQLTu", + FunctionArgType::Other { .. } => "PRnJhw", + } + } + + fn extract_function_info(code: &str) -> FunctionInfo { + let problem_code: ProblemCode = code.to_string().try_into().expect("Should be valid code"); + + if let ProblemType::NonDesign(info) = problem_code.type_ { + info + } else { + panic!("Expected Non Design Problem") + } + } + + #[test] + fn function_parsing() { + // Arrange + let code = create_code_stub_all_arg_types_non_design(); + + // Create hashset and fill with the possible argument types + let mut left_to_see = HashSet::new(); + FunctionArgType::iter().for_each(|x| { + left_to_see.insert(x); + }); + + // Add special handling for Other variant + left_to_see.remove(&FunctionArgType::Other { + raw: "".to_string(), + }); + left_to_see.insert(FunctionArgType::Other { + raw: "UnknownType".to_string(), + }); + + // Act + let fn_info = extract_function_info(code); + + // Assert + assert_eq!(fn_info.name, "func_name"); + assert!(fn_info.return_type.is_none()); + for arg in fn_info.fn_args.args.iter() { + if !left_to_see.contains(&arg.arg_type) { + panic!("Duplicate type seen. Each type should show up EXACTLY ONCE. Duplicate type: {}",arg.arg_type); + } + left_to_see.remove(&arg.arg_type); + assert_eq!( + arg.identifier, + fn_type_to_id(&arg.arg_type), + "ArgType: {}", + arg.arg_type + ); + } + assert!( + left_to_see.is_empty(), + "Expected all argument types to be seen but haven't seen {left_to_see:?}", + ); + } + + #[test] + fn function_arg_type_apply() { + // Using an array instead of rstest because we need to ensure all inputs are + // covered + use FunctionArgType::*; + let inputs = [ + (I32, "1"), + (I64, "2"), + (F64, "2.00000"), + (Bool, "true"), + (String_, "\"leetcode\""), + (VecI32, "[1,2,3,4]"), + (VecF64, "[6.00000,0.50000,-1.00000,1.00000,-1.00000]"), + (VecBool, "[true,false,false,false,false]"), + (VecString, "[\"@..aA\",\"..B#.\",\"....b\"]"), + (VecVecI32, "[[2,2,3],[7]]"), + ( + VecVecString, + "[[\"java\"],[\"nodejs\"],[\"nodejs\",\"reactjs\"]]", + ), + (List, "[1,2,4]"), + (Tree, "[1,null,2,3]"), + (Other { raw: "".into() }, "1"), + ]; + + // Create hashset and fill with the possible argument types + let mut left_to_see = HashSet::new(); + FunctionArgType::iter().for_each(|x| { + left_to_see.insert(x); + }); + + // Ensure each is there exactly once + for (fat, _) in inputs.iter() { + if !left_to_see.contains(fat) { + panic!("Duplicate type seen. Each type should show up EXACTLY ONCE. Duplicate type: {fat}"); + } + left_to_see.remove(fat); + } + assert!( + left_to_see.is_empty(), + "Expected all argument types to be seen but haven't seen {left_to_see:?}", + ); + + for (fat, input) in inputs { + let expected = match fat { + I32 => "1", + I64 => "2", + F64 => "2.00000", + Bool => "true", + String_ => "\"leetcode\"", + VecI32 => "vec![1,2,3,4]", + VecF64 => "vec![6.00000,0.50000,-1.00000,1.00000,-1.00000]", + VecBool => "vec![true,false,false,false,false]", + VecString => "vec![\"@..aA\".into(),\"..B#.\".into(),\"....b\".into()]", + VecVecI32 => "vec![vec![2,2,3],vec![7]]", + VecVecString => { + "vec![vec![\"java\".into()],vec![\"nodejs\".into()],vec![\"nodejs\".into(),\"reactjs\".into()]]" + } + List => "ListHead::from(vec![1,2,4]).into()", + Tree => "TreeRoot::from(\"[1,null,2,3]\").into()", + Other { raw: _ } => "todo!(\"1\")", + }; + let actual = fat.apply(input).expect("Should be valid input"); + assert_eq!(actual, expected); + } + } +} diff --git a/src/tool/core/helpers/problem_metadata.rs b/src/tool/core/helpers/problem_metadata.rs new file mode 100644 index 0000000..a8095b7 --- /dev/null +++ b/src/tool/core/helpers/problem_metadata.rs @@ -0,0 +1,144 @@ +use crate::tool::{config::Config, core::helpers::problem_code::ProblemType}; +use anyhow::Context; +use log::{debug, info}; +use serde::Deserialize; +use serde_flat_path::flat_path; + +use super::problem_code::{FunctionInfo, ProblemCode}; + +/// This struct is only used because there are two fields that we are interested +/// in that start with the same path and flat_path does not support that yet +#[flat_path] +#[derive(Deserialize, Debug)] +struct QuestionWrapper { + #[flat_path("data.question")] + inner: ProblemMetadata, +} + +#[flat_path] +#[derive(Deserialize, Debug)] +pub(crate) struct ProblemMetadata { + #[serde(rename = "questionFrontendId")] + id: String, + #[serde(rename = "questionTitle")] + title: String, + #[serde(rename = "exampleTestcaseList")] + example_test_case_list: Vec, +} + +impl ProblemMetadata { + /// Checks if the data is valid + fn validate(&self) -> anyhow::Result<()> { + let _: u16 = self.get_id()?; + Ok(()) + } + + pub(crate) fn get_id(&self) -> anyhow::Result { + let result = self + .id + .parse() + .with_context(|| format!("ID is not a valid u16. Got: {}", self.id))?; + Ok(result) + } + + pub(crate) fn get_num_and_title(&self) -> anyhow::Result { + Ok(format!("{}. {}", self.get_id()?, self.title)) + } + + pub(crate) fn get_test_cases(&self, problem_code: &ProblemCode) -> anyhow::Result { + info!("Going to get tests"); + + let mut imports = String::new(); + + let tests = match &problem_code.type_ { + ProblemType::NonDesign(fn_info) => { + // Add imports + if problem_code.has_tree() { + imports.push_str("use cargo_leet::TreeRoot;\n"); + } + if problem_code.has_list() { + imports.push_str("use cargo_leet::ListHead;\n"); + } + + // Add actual test cases + self.get_test_cases_is_not_design(fn_info) + .context("Failed to get test cases for non-design problem")? + } + ProblemType::Design => self + .get_test_cases_is_design() + .context("Failed to get test cases for design problem")?, + }; + + Ok(format!( + r#" +#[cfg(test)] +mod tests {{ + use super::*; + {imports} + + {tests} +}} +"# + )) + } + + fn get_test_cases_is_not_design(&self, fn_info: &FunctionInfo) -> anyhow::Result { + let mut result = "use rstest::rstest; + + #[rstest] +" + .to_string(); + + // Add test cases + for example_test_case_raw in self.example_test_case_list.iter() { + let test_case = fn_info + .get_test_case(example_test_case_raw) + .context("Failed to convert downloaded test case into macro of input")?; + result.push_str(&format!(" #[case({})]\n", test_case)) + } + + // Add test case function body + let test_fn = format!( + " fn case({}) {{ + let actual = Solution::{}({}); + {} + }}", + fn_info.get_args_with_case(), + fn_info.name, + fn_info.get_args_names(), + fn_info.get_solution_comparison_code(), + ); + result.push_str(&test_fn); + + Ok(result) + } + + fn get_test_cases_is_design(&self) -> anyhow::Result { + Ok("".to_string()) + } +} + +pub(crate) fn get_problem_metadata(title_slug: &str) -> anyhow::Result { + info!("Going to get problem metadata"); + let QuestionWrapper { inner: result } = ureq::get(Config::LEETCODE_GRAPH_QL) + .send_json(ureq::json!({ + "query": r#"query consolePanelConfig($titleSlug: String!) { + question(titleSlug: $titleSlug) { + questionFrontendId + questionTitle + exampleTestcaseList + } + }"#, + "variables":{"titleSlug": title_slug}, + "operationName":"consolePanelConfig" + })) + .context("Get request for problem metadata failed")? + .into_json() + .context("Failed to convert response from json to problem metadata")?; + + result + .validate() + .context("Failed to validate problem metadata")?; + debug!("ProblemMetadata built: {result:#?}"); + Ok(result) +} diff --git a/src/tool/core/helpers/write_to_disk.rs b/src/tool/core/helpers/write_to_disk.rs new file mode 100644 index 0000000..f678217 --- /dev/null +++ b/src/tool/core/helpers/write_to_disk.rs @@ -0,0 +1,61 @@ +use anyhow::Context; +use log::{error, info}; +use std::{ + env, + fs::{remove_file, OpenOptions}, + io::Write, + path::PathBuf, + process::Command, +}; + +fn update_lib(module_name: &str) -> anyhow::Result<()> { + info!("Adding {module_name} to libs.rs"); + let lib_path = PathBuf::from("src/lib.rs"); + let mut lib = OpenOptions::new() + .append(true) + .open(&lib_path) + .with_context(|| { + format!( + "Failed to open {:?}", + env::current_dir() + .expect("Unable to resolve current directory") + .join(lib_path) + ) + })?; + let _ = lib.write(format!("pub mod {module_name};").as_bytes())?; + Ok(()) +} + +pub(crate) fn write_file(module_name: &str, module_code: String) -> anyhow::Result<()> { + info!("Writing code to disk for module {module_name}"); + let path = PathBuf::from(format!("src/{module_name}.rs")); + let mut file = OpenOptions::new() + .write(true) + .create_new(true) + .open(&path) + .with_context(|| format!("Failed to create '{}'", path.display()))?; + file.write_all(module_code.as_bytes()) + .with_context(|| format!("Failed writing to '{}'", path.display()))?; + let lib_update_status = update_lib(module_name); + if lib_update_status.is_err() { + error!("Failed to update lib.rs: Performing cleanup of partially completed command"); + // clean up + remove_file(&path).with_context(|| { + format!( + "Failed to remove '{}' during cleanup after failing to update lib.rs", + path.display() + ) + })?; + lib_update_status.context( + "Failed to update lib.rs. Does the file exists? Is it able to be written to?", + )?; + } + + info!("Going to run rustfmt on files"); + Command::new("cargo") + .arg("fmt") + .arg("--all") + .output() + .context("Error running rustfmt")?; + Ok(()) +} diff --git a/src/tool/core/mod.rs b/src/tool/core/mod.rs new file mode 100644 index 0000000..d5022d4 --- /dev/null +++ b/src/tool/core/mod.rs @@ -0,0 +1,34 @@ +mod generate; +mod helpers; + +use self::generate::do_generate; +use crate::tool::cli::{self, Cli}; +use anyhow::{bail, Context}; +use std::{env, path::Path}; + +/// Entry point used by the tool. The `main.rs` is pretty thin shim around this +/// function. +pub fn run(cli: &Cli) -> anyhow::Result<()> { + cli.update_current_working_dir()?; + + working_directory_validation()?; + + match &cli.command { + cli::Commands::Generate(args) => do_generate(args), + } +} + +fn working_directory_validation() -> anyhow::Result<()> { + let req_file = "Cargo.toml"; + let path = Path::new(req_file); + if path.exists() { + Ok(()) + } else { + bail!( + "Failed to find {req_file} in current directory '{}'", + env::current_dir() + .context("Failed to get current working directory")? + .display() + ); + } +} diff --git a/src/tool/log.rs b/src/tool/log.rs new file mode 100644 index 0000000..7317339 --- /dev/null +++ b/src/tool/log.rs @@ -0,0 +1,9 @@ +#![cfg(feature = "tool")] +use env_logger::Builder; +use log::LevelFilter; + +/// Initializes the logging based on the log level passed +pub fn init_logging(level: LevelFilter) -> anyhow::Result<()> { + Builder::new().filter(None, level).try_init()?; + Ok(()) +} diff --git a/src/tool/mod.rs b/src/tool/mod.rs new file mode 100644 index 0000000..6850096 --- /dev/null +++ b/src/tool/mod.rs @@ -0,0 +1,4 @@ +pub(crate) mod cli; +pub(crate) mod config; +pub(crate) mod core; +pub(crate) mod log; diff --git a/src/write_file.rs b/src/write_file.rs deleted file mode 100644 index 4c1f866..0000000 --- a/src/write_file.rs +++ /dev/null @@ -1,41 +0,0 @@ -use convert_case::{Case, Casing}; -use std::{ - error::Error, - fs::{remove_file, OpenOptions}, - io::Write, - path::PathBuf, - process::Command, -}; - -fn update_lib(slug_snake: &str) -> Result<(), Box> { - let lib_path = PathBuf::from(format!("{}/../src/lib.rs", env!("CARGO_MANIFEST_DIR"))); - let mut lib = OpenOptions::new().append(true).open(lib_path)?; - let _ = lib.write(format!("pub mod {slug_snake};").as_bytes())?; - Ok(()) -} - -pub fn write_file(title_slug: &str, code_snippet: String) -> Result<(), Box> { - let slug_snake = title_slug.to_case(Case::Snake); - let path = PathBuf::from(format!( - "{}/../src/{slug_snake}.rs", - env!("CARGO_MANIFEST_DIR") - )); - let mut file = OpenOptions::new() - .write(true) - .create_new(true) - .open(path.clone())?; - file.write_all(code_snippet.as_bytes())?; - let output = update_lib(&slug_snake); - if output.is_err() { - // clean up - remove_file(path)?; - output?; - } - Command::new("cargo") - .arg("fmt") - .arg("--all") - .current_dir(format!("{}/../", env!("CARGO_MANIFEST_DIR"))) - .spawn()? - .wait()?; - Ok(()) -}