├── .github ├── media │ └── simple_struct_demo.png └── workflows │ ├── code_style.yml │ ├── release-plz.yml │ └── tests.yml ├── .gitignore ├── CHANGELOG.md ├── CODEOWNERS ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples ├── advanced_enum.rs ├── advanced_struct.rs ├── simple_enum.rs ├── simple_struct.rs ├── struct_with_context.rs ├── struct_with_flatten.rs ├── struct_with_named_arg.rs ├── struct_with_subargs.rs ├── struct_with_subcommand.rs └── to_cli_args.rs ├── interactive-clap-derive ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT └── src │ ├── debug.rs │ ├── derives │ ├── interactive_clap │ │ ├── common_methods │ │ │ ├── choose_variant.rs │ │ │ ├── fields_with_skip_default_input_arg.rs │ │ │ ├── from_cli_for_enum.rs │ │ │ ├── interactive_clap_attrs_context.rs │ │ │ └── mod.rs │ │ ├── mod.rs │ │ └── structs │ │ │ ├── clap_for_named_arg_enum.rs │ │ │ ├── common_field_methods │ │ │ ├── mod.rs │ │ │ ├── with_skip_interactive_input.rs │ │ │ ├── with_subargs.rs │ │ │ └── with_subcommand.rs │ │ │ ├── from_cli_trait.rs │ │ │ ├── input_args_impl.rs │ │ │ ├── to_cli_trait │ │ │ ├── clap_parser_trait_adapter.rs │ │ │ ├── cli_variant_struct │ │ │ │ ├── field.rs │ │ │ │ └── mod.rs │ │ │ ├── from_trait.rs │ │ │ └── mod.rs │ │ │ └── to_interactive_clap_context_scope_trait.rs │ ├── mod.rs │ └── to_cli_args │ │ ├── methods │ │ ├── interactive_clap_attrs_cli_field.rs │ │ └── mod.rs │ │ └── mod.rs │ ├── helpers │ ├── mod.rs │ ├── snake_case_to_camel_case.rs │ └── to_kebab_case.rs │ ├── lib.rs │ └── tests │ ├── mod.rs │ ├── snapshots │ ├── interactive_clap_derive__tests__test_simple_enum__simple_enum-2.snap │ ├── interactive_clap_derive__tests__test_simple_enum__simple_enum.snap │ ├── interactive_clap_derive__tests__test_simple_enum__simple_enum_with_strum_discriminants-2.snap │ ├── interactive_clap_derive__tests__test_simple_enum__simple_enum_with_strum_discriminants.snap │ ├── interactive_clap_derive__tests__test_simple_struct__bug_fix_of_to_cli_args_derive-2.snap │ ├── interactive_clap_derive__tests__test_simple_struct__bug_fix_of_to_cli_args_derive.snap │ ├── interactive_clap_derive__tests__test_simple_struct__doc_comments_propagate-2.snap │ ├── interactive_clap_derive__tests__test_simple_struct__doc_comments_propagate.snap │ ├── interactive_clap_derive__tests__test_simple_struct__flag-2.snap │ ├── interactive_clap_derive__tests__test_simple_struct__flag.snap │ ├── interactive_clap_derive__tests__test_simple_struct__simple_struct-2.snap │ ├── interactive_clap_derive__tests__test_simple_struct__simple_struct.snap │ ├── interactive_clap_derive__tests__test_simple_struct__simple_struct_with_named_arg-2.snap │ ├── interactive_clap_derive__tests__test_simple_struct__simple_struct_with_named_arg.snap │ ├── interactive_clap_derive__tests__test_simple_struct__vec_multiple_opt-2.snap │ └── interactive_clap_derive__tests__test_simple_struct__vec_multiple_opt.snap │ ├── test_simple_enum.rs │ └── test_simple_struct.rs ├── rust-toolchain.toml └── src ├── helpers ├── mod.rs ├── snake_case_to_camel_case.rs └── to_kebab_case.rs └── lib.rs /.github/media/simple_struct_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/near-cli-rs/interactive-clap/a8b0542c08b27a503d95cbc06f2812057bfb6b32/.github/media/simple_struct_demo.png -------------------------------------------------------------------------------- /.github/workflows/code_style.yml: -------------------------------------------------------------------------------- 1 | name: Code Style 2 | on: 3 | pull_request: 4 | 5 | jobs: 6 | codestyle: 7 | name: Code Style (fmt + clippy) 8 | runs-on: ubuntu-20.04 9 | steps: 10 | - name: Checkout repository 11 | uses: actions/checkout@v2 12 | - name: Install Rust 13 | uses: actions-rs/toolchain@v1 14 | with: 15 | toolchain: stable 16 | override: true 17 | profile: minimal 18 | components: rustfmt 19 | - name: Check formatting 20 | run: | 21 | cargo fmt --all -- --check 22 | - name: Install libudev-dev 23 | run: | 24 | sudo apt-get update 25 | sudo apt-get install --assume-yes libudev-dev 26 | - name: Check lints (cargo clippy) 27 | run: cargo clippy --examples -- -D warnings 28 | 29 | -------------------------------------------------------------------------------- /.github/workflows/release-plz.yml: -------------------------------------------------------------------------------- 1 | name: Release-plz 2 | 3 | permissions: 4 | pull-requests: write 5 | contents: write 6 | 7 | on: 8 | push: 9 | branches: 10 | - master 11 | 12 | jobs: 13 | release-plz: 14 | name: Release-plz 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v3 19 | with: 20 | fetch-depth: 0 21 | token: ${{ secrets.CUSTOM_GITHUB_TOKEN }} 22 | - name: Install Rust toolchain 23 | uses: dtolnay/rust-toolchain@stable 24 | - name: Run release-plz 25 | uses: MarcoIeni/release-plz-action@v0.5 26 | env: 27 | # https://marcoieni.github.io/release-plz/github-action.html#triggering-further-workflow-runs 28 | GITHUB_TOKEN: ${{ secrets.CUSTOM_GITHUB_TOKEN }} 29 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 30 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | pull_request: 4 | 5 | env: 6 | CARGO_TERM_COLOR: always 7 | 8 | jobs: 9 | tests: 10 | name: Tests 11 | runs-on: ubuntu-20.04 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v2 15 | - name: Install Rust 16 | uses: actions-rs/toolchain@v1 17 | with: 18 | toolchain: stable 19 | override: true 20 | profile: minimal 21 | - name: Tests 22 | run: cargo test --workspace 23 | # there're sometimes warnings, which signal, that the generated doc 24 | # won't look as expected, when rendered, and sometimes errors, which will prevent doc from being 25 | # generated at release time altogether. 26 | cargo-doc: 27 | runs-on: ubuntu-20.04 28 | 29 | steps: 30 | - uses: actions/checkout@v4 31 | - name: Install Toolchain 32 | uses: actions-rs/toolchain@v1 33 | with: 34 | profile: minimal 35 | toolchain: stable 36 | default: true 37 | - name: run cargo doc 38 | env: 39 | RUSTDOCFLAGS: -D warnings 40 | run: | 41 | cargo doc -p interactive-clap 42 | cargo doc -p interactive-clap-derive --document-private-items 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .DS_Store 3 | Cargo.lock 4 | *.snap.new 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.3.2](https://github.com/near-cli-rs/interactive-clap/compare/interactive-clap-v0.3.1...interactive-clap-v0.3.2) - 2025-02-11 10 | 11 | ### Added 12 | 13 | - propagate doc comments on flags and arguments to `--help/-h` + structs derive refactor (#26) 14 | 15 | ### Other 16 | 17 | - Added "cargo test" command (#33) 18 | - Added clippy to examples (#31) 19 | - Added code style check (#29) 20 | - add CODEOWNERS (#27) 21 | - Added a demo image to README (#24) 22 | 23 | ## [0.3.1](https://github.com/near-cli-rs/interactive-clap/compare/interactive-clap-v0.3.0...interactive-clap-v0.3.1) - 2024-09-18 24 | 25 | ### Added 26 | 27 | - add `long_vec_multiple_opt` attribute ([#22](https://github.com/near-cli-rs/interactive-clap/pull/22)) 28 | 29 | ## [0.3.0](https://github.com/near-cli-rs/interactive-clap/compare/interactive-clap-v0.2.10...interactive-clap-v0.3.0) - 2024-08-09 30 | 31 | ### Fixed 32 | - [**breaking**] Proxy `try_parse_from` to Clap's `try_parse_from` as is, instead of naive parsing of `&str` ([#21](https://github.com/near-cli-rs/interactive-clap/pull/21)) 33 | 34 | ### Other 35 | - Updated examples:struct_with_flatten ([#19](https://github.com/near-cli-rs/interactive-clap/pull/19)) 36 | 37 | ## [0.2.10](https://github.com/near-cli-rs/interactive-clap/compare/interactive-clap-v0.2.9...interactive-clap-v0.2.10) - 2024-04-21 38 | 39 | ### Added 40 | - Add support for "subargs" ([#17](https://github.com/near-cli-rs/interactive-clap/pull/17)) 41 | 42 | ## [0.2.9](https://github.com/near-cli-rs/interactive-clap/compare/interactive-clap-v0.2.8...interactive-clap-v0.2.9) - 2024-03-25 43 | 44 | ### Added 45 | - Added support for "#[interactive_clap(flatten)]" ([#15](https://github.com/near-cli-rs/interactive-clap/pull/15)) 46 | 47 | ## [0.2.8](https://github.com/near-cli-rs/interactive-clap/compare/interactive-clap-v0.2.7...interactive-clap-v0.2.8) - 2024-01-15 48 | 49 | ### Added 50 | - Added possibility to process optional fields ([#13](https://github.com/near-cli-rs/interactive-clap/pull/13)) 51 | 52 | ## [0.2.7](https://github.com/near-cli-rs/interactive-clap/compare/interactive-clap-v0.2.6...interactive-clap-v0.2.7) - 2023-10-13 53 | 54 | ### Other 55 | - updated the following local packages: interactive-clap-derive 56 | 57 | ## [0.2.6](https://github.com/near-cli-rs/interactive-clap/compare/interactive-clap-v0.2.5...interactive-clap-v0.2.6) - 2023-10-05 58 | 59 | ### Fixed 60 | - named_args/unnamed_args/args_without_attrs conflict ([#9](https://github.com/near-cli-rs/interactive-clap/pull/9)) 61 | 62 | ## [0.2.5](https://github.com/near-cli-rs/interactive-clap/compare/interactive-clap-v0.2.4...interactive-clap-v0.2.5) - 2023-09-21 63 | 64 | ### Other 65 | - added fn try_parse_from() 66 | - Merge branch 'master' of https://github.com/FroVolod/interactive-clap 67 | 68 | ## [0.2.4](https://github.com/near-cli-rs/interactive-clap/compare/interactive-clap-v0.2.3...interactive-clap-v0.2.4) - 2023-06-02 69 | 70 | ### Added 71 | - Add support for boolean flags (e.g. --offline) ([#6](https://github.com/near-cli-rs/interactive-clap/pull/6)) 72 | 73 | ## [0.2.3](https://github.com/near-cli-rs/interactive-clap/compare/interactive-clap-v0.2.2...interactive-clap-v0.2.3) - 2023-05-30 74 | 75 | ### Other 76 | - Added README 77 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @FroVolod @akorchyn @dj8yfo @PolyProgrammist 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "interactive-clap-derive", 4 | ] 5 | 6 | [package] 7 | name = "interactive-clap" 8 | version = "0.3.2" 9 | authors = ["FroVolod "] 10 | edition = "2018" 11 | license = "MIT OR Apache-2.0" 12 | repository = "https://github.com/FroVolod/interactive-clap" 13 | description = "Interactive mode extension crate to Command Line Arguments Parser (https://crates.io/crates/clap)" 14 | 15 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 16 | 17 | [dependencies] 18 | interactive-clap-derive = { path = "interactive-clap-derive", version = "0.3.2" } 19 | strum = { version = "0.24", features = ["derive"] } 20 | strum_macros = "0.24" 21 | 22 | [dev-dependencies] 23 | shell-words = "1.0.0" 24 | 25 | clap = { version = "4.0.18", features = ["derive"] } 26 | 27 | inquire = "0.6" 28 | color-eyre = "0.6" 29 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2022 Volodymyr Frolov 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021-2022 Volodymyr Frolov and Interactive-Clap Contributors 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 | # Interactive clap 2 | 3 | > **`interactive-clap` is a Rust crate of helpers for [`clap`](https://github.com/clap-rs/clap) that enable interactive prompts for structs.** 4 | 5 | [![Crates.io](https://img.shields.io/crates/v/interactive-clap?style=flat-square)](https://crates.io/crates/interactive-clap) 6 | [![Crates.io](https://img.shields.io/crates/d/interactive-clap?style=flat-square)](https://crates.io/crates/interactive-clap) 7 | [![License](https://img.shields.io/badge/license-Apache%202.0-blue?style=flat-square)](LICENSE-APACHE) 8 | [![License](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](LICENSE-MIT) 9 | [![Contributors](https://img.shields.io/github/contributors/near-cli-rs/interactive-clap?style=flat-square)](https://github.com/near-cli-rs/interactive-clap/graphs/contributors) 10 | 11 | See examples in the [`examples/`](https://github.com/near-cli-rs/interactive-clap/tree/master/examples) folder. 12 | 13 |

14 | Usage example showing a simple struct annotated with interactive-clap 15 |

16 | 17 | See it in action in [`near-cli-rs`](https://near.cli.rs) and [`bos-cli-rs`](https://bos.cli.rs). 18 | -------------------------------------------------------------------------------- /examples/advanced_enum.rs: -------------------------------------------------------------------------------- 1 | //This example shows how to parse data from the command line to an enum using the "interactive-clap" macro. 2 | 3 | // 1) build an example: cargo build --example advanced_enum 4 | // 2) go to the `examples` folder: cd target/debug/examples 5 | // 3) run an example: ./advanced_enum (without parameters) => entered interactive mode 6 | // ./advanced_enum network 23 QWE ASDFG => mode: Network(CliArgs { age: Some(23), first_name: Some("QWE"), second_name: Some("ASDFG") }) 7 | // ./advanced_enum offline => mode: Offline 8 | // To learn more about the parameters, use "help" flag: ./advanced_enum --help 9 | 10 | use interactive_clap::{ResultFromCli, ToCliArgs}; 11 | use strum::{EnumDiscriminants, EnumIter, EnumMessage}; 12 | 13 | #[derive(Debug, EnumDiscriminants, Clone, interactive_clap::InteractiveClap)] 14 | #[strum_discriminants(derive(EnumMessage, EnumIter))] 15 | ///To construct a transaction you will need to provide information about sender (signer) and receiver accounts, and actions that needs to be performed. 16 | ///Do you want to derive some information required for transaction construction automatically querying it online? 17 | pub enum Mode { 18 | /// Prepare and, optionally, submit a new transaction with online mode 19 | #[strum_discriminants(strum(message = "Yes, I keep it simple"))] 20 | Network(Args), 21 | /// Prepare and, optionally, submit a new transaction with offline mode 22 | #[strum_discriminants(strum( 23 | message = "No, I want to work in no-network (air-gapped) environment" 24 | ))] 25 | Offline, 26 | } 27 | 28 | #[derive(Debug, Clone, interactive_clap::InteractiveClap)] 29 | pub struct Args { 30 | age: u64, 31 | first_name: String, 32 | second_name: String, 33 | } 34 | 35 | fn main() -> color_eyre::Result<()> { 36 | let cli_mode = Mode::try_parse().ok(); 37 | let context = (); // default: input_context = () 38 | let mode = loop { 39 | let mode = ::from_cli(cli_mode.clone(), context); 40 | match mode { 41 | ResultFromCli::Ok(cli_mode) => break cli_mode, 42 | ResultFromCli::Cancel(Some(cli_mode)) => { 43 | println!( 44 | "Your console command: {}", 45 | shell_words::join(cli_mode.to_cli_args()) 46 | ); 47 | return Ok(()); 48 | } 49 | ResultFromCli::Cancel(None) => { 50 | println!("Goodbye!"); 51 | return Ok(()); 52 | } 53 | ResultFromCli::Back => {} 54 | ResultFromCli::Err(optional_cli_mode, err) => { 55 | if let Some(cli_mode) = optional_cli_mode { 56 | println!( 57 | "Your console command: {}", 58 | shell_words::join(cli_mode.to_cli_args()) 59 | ); 60 | } 61 | return Err(err); 62 | } 63 | } 64 | }; 65 | println!("mode: {:?}", mode); 66 | println!( 67 | "Your console command: {}", 68 | shell_words::join(mode.to_cli_args()) 69 | ); 70 | Ok(()) 71 | } 72 | -------------------------------------------------------------------------------- /examples/advanced_struct.rs: -------------------------------------------------------------------------------- 1 | // This example shows additional functionality of the "interactive-clap" macro for parsing command-line data into a structure using macro attributes. 2 | 3 | // 1) build an example: cargo build --example advanced_struct 4 | // 2) go to the `examples` folder: cd target/debug/examples 5 | // 3) run an example: ./advanced_struct (without parameters) => entered interactive mode 6 | // ./advanced_struct --age-full-years 30 --first-name QWE --second-name QWERTY --favorite-color red => 7 | // => cli_args: CliArgs { age: Some(30), first_name: Some("QWE"), second_name: Some("QWERTY"), favorite_color: Some(Red) } 8 | // ./advanced_struct --first-name QWE --second-name QWERTY --favorite-color red => 9 | // => cli_args: CliArgs { age: None, first_name: Some("QWE"), second_name: Some("QWERTY"), favorite_color: Some(Red) } 10 | // To learn more about the parameters, use "help" flag: ./advanced_struct --help 11 | 12 | use inquire::Select; 13 | use interactive_clap::{ResultFromCli, ToCliArgs}; 14 | use strum::{EnumDiscriminants, EnumIter, EnumMessage, IntoEnumIterator}; 15 | 16 | #[derive(Debug, Clone, interactive_clap::InteractiveClap)] 17 | struct Args { 18 | #[interactive_clap(long = "age-full-years")] 19 | #[interactive_clap(skip_interactive_input)] 20 | /// If you want, enter the full age on the command line 21 | age: Option, 22 | #[interactive_clap(long)] 23 | /// What is your first name? 24 | first_name: String, 25 | #[interactive_clap(long)] 26 | #[interactive_clap(skip_default_input_arg)] 27 | second_name: String, 28 | #[interactive_clap(long)] 29 | #[interactive_clap(value_enum)] 30 | #[interactive_clap(skip_default_input_arg)] 31 | favorite_color: ColorPalette, 32 | } 33 | 34 | impl Args { 35 | fn input_second_name(_context: &()) -> color_eyre::eyre::Result> { 36 | match inquire::Text::new("Input second name".to_string().as_str()).prompt() { 37 | Ok(value) => Ok(Some(value)), 38 | Err( 39 | inquire::error::InquireError::OperationCanceled 40 | | inquire::error::InquireError::OperationInterrupted, 41 | ) => Ok(None), 42 | Err(err) => Err(err.into()), 43 | } 44 | } 45 | 46 | fn input_favorite_color(_context: &()) -> color_eyre::eyre::Result> { 47 | let variants = ColorPaletteDiscriminants::iter().collect::>(); 48 | let selected = Select::new("What color is your favorite?", variants).prompt()?; 49 | match selected { 50 | ColorPaletteDiscriminants::Red => Ok(Some(ColorPalette::Red)), 51 | ColorPaletteDiscriminants::Orange => Ok(Some(ColorPalette::Orange)), 52 | ColorPaletteDiscriminants::Yellow => Ok(Some(ColorPalette::Yellow)), 53 | ColorPaletteDiscriminants::Green => Ok(Some(ColorPalette::Green)), 54 | ColorPaletteDiscriminants::Blue => Ok(Some(ColorPalette::Blue)), 55 | ColorPaletteDiscriminants::Indigo => Ok(Some(ColorPalette::Indigo)), 56 | ColorPaletteDiscriminants::Violet => Ok(Some(ColorPalette::Violet)), 57 | } 58 | } 59 | } 60 | 61 | #[derive(Debug, EnumDiscriminants, Clone, clap::ValueEnum)] 62 | #[strum_discriminants(derive(EnumMessage, EnumIter))] 63 | pub enum ColorPalette { 64 | #[strum_discriminants(strum(message = "red"))] 65 | /// Red 66 | Red, 67 | #[strum_discriminants(strum(message = "orange"))] 68 | /// Orange 69 | Orange, 70 | #[strum_discriminants(strum(message = "yellow"))] 71 | /// Yellow 72 | Yellow, 73 | #[strum_discriminants(strum(message = "green"))] 74 | /// Green 75 | Green, 76 | #[strum_discriminants(strum(message = "blue"))] 77 | /// Blue 78 | Blue, 79 | #[strum_discriminants(strum(message = "indigo"))] 80 | /// Indigo 81 | Indigo, 82 | #[strum_discriminants(strum(message = "violet"))] 83 | /// Violet 84 | Violet, 85 | } 86 | 87 | impl interactive_clap::ToCli for ColorPalette { 88 | type CliVariant = ColorPalette; 89 | } 90 | 91 | impl std::str::FromStr for ColorPalette { 92 | type Err = String; 93 | fn from_str(s: &str) -> Result { 94 | match s { 95 | "red" => Ok(Self::Red), 96 | "orange" => Ok(Self::Orange), 97 | "yellow" => Ok(Self::Yellow), 98 | "green" => Ok(Self::Green), 99 | "blue" => Ok(Self::Blue), 100 | "indigo" => Ok(Self::Indigo), 101 | "violet" => Ok(Self::Violet), 102 | _ => Err("ColorPalette: incorrect value entered".to_string()), 103 | } 104 | } 105 | } 106 | 107 | impl std::fmt::Display for ColorPalette { 108 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 109 | match self { 110 | Self::Red => write!(f, "red"), 111 | Self::Orange => write!(f, "orange"), 112 | Self::Yellow => write!(f, "yellow"), 113 | Self::Green => write!(f, "green"), 114 | Self::Blue => write!(f, "blue"), 115 | Self::Indigo => write!(f, "indigo"), 116 | Self::Violet => write!(f, "violet"), 117 | } 118 | } 119 | } 120 | 121 | impl std::fmt::Display for ColorPaletteDiscriminants { 122 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 123 | match self { 124 | Self::Red => write!(f, "red"), 125 | Self::Orange => write!(f, "orange"), 126 | Self::Yellow => write!(f, "yellow"), 127 | Self::Green => write!(f, "green"), 128 | Self::Blue => write!(f, "blue"), 129 | Self::Indigo => write!(f, "indigo"), 130 | Self::Violet => write!(f, "violet"), 131 | } 132 | } 133 | } 134 | 135 | fn main() -> color_eyre::Result<()> { 136 | let mut cli_args = Args::parse(); 137 | let context = (); // default: input_context = () 138 | loop { 139 | let args = ::from_cli(Some(cli_args), context); 140 | match args { 141 | ResultFromCli::Ok(cli_args) | ResultFromCli::Cancel(Some(cli_args)) => { 142 | println!("cli_args: {cli_args:?}"); 143 | println!( 144 | "Your console command: {}", 145 | shell_words::join(cli_args.to_cli_args()) 146 | ); 147 | return Ok(()); 148 | } 149 | ResultFromCli::Cancel(None) => { 150 | println!("Goodbye!"); 151 | return Ok(()); 152 | } 153 | ResultFromCli::Back => { 154 | cli_args = Default::default(); 155 | } 156 | ResultFromCli::Err(cli_args, err) => { 157 | if let Some(cli_args) = cli_args { 158 | println!( 159 | "Your console command: {}", 160 | shell_words::join(cli_args.to_cli_args()) 161 | ); 162 | } 163 | return Err(err); 164 | } 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /examples/simple_enum.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | //This example shows how to parse data from the command line to an enum using the "interactive-clap" macro. 3 | 4 | // 1) build an example: cargo build --example simple_enum 5 | // 2) go to the `examples` folder: cd target/debug/examples 6 | // 3) run an example: ./simple_enum (without parameters) => entered interactive mode 7 | // ./simple_enum network => mode: Ok(Network) 8 | // ./simple_enum offline => mode: Ok(Offline) 9 | // To learn more about the parameters, use "help" flag: ./simple_enum --help 10 | 11 | use interactive_clap::{ResultFromCli, ToCliArgs}; 12 | use strum::{EnumDiscriminants, EnumIter, EnumMessage}; 13 | 14 | #[derive(Debug, EnumDiscriminants, Clone, interactive_clap::InteractiveClap)] 15 | #[strum_discriminants(derive(EnumMessage, EnumIter))] 16 | ///To construct a transaction you will need to provide information about sender (signer) and receiver accounts, and actions that needs to be performed. 17 | ///Do you want to derive some information required for transaction construction automatically querying it online? 18 | pub enum Mode { 19 | /// Prepare and, optionally, submit a new transaction with online mode 20 | #[strum_discriminants(strum(message = "Yes, I keep it simple"))] 21 | Network, 22 | /// Prepare and, optionally, submit a new transaction with offline mode 23 | #[strum_discriminants(strum( 24 | message = "No, I want to work in no-network (air-gapped) environment" 25 | ))] 26 | Offline, 27 | } 28 | 29 | fn main() -> color_eyre::Result<()> { 30 | let cli_mode = Mode::try_parse().ok(); 31 | let context = (); // default: input_context = () 32 | loop { 33 | let mode = ::from_cli(cli_mode.clone(), context); 34 | match mode { 35 | ResultFromCli::Ok(cli_mode) | ResultFromCli::Cancel(Some(cli_mode)) => { 36 | println!( 37 | "Your console command: {}", 38 | shell_words::join(cli_mode.to_cli_args()) 39 | ); 40 | return Ok(()); 41 | } 42 | ResultFromCli::Cancel(None) => { 43 | println!("Goodbye!"); 44 | return Ok(()); 45 | } 46 | ResultFromCli::Back => {} 47 | ResultFromCli::Err(optional_cli_mode, err) => { 48 | if let Some(cli_mode) = optional_cli_mode { 49 | println!( 50 | "Your console command: {}", 51 | shell_words::join(cli_mode.to_cli_args()) 52 | ); 53 | } 54 | return Err(err); 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /examples/simple_struct.rs: -------------------------------------------------------------------------------- 1 | // This example shows how to parse data from the command line to a structure using the "interactive-clap" macro. 2 | 3 | // 1) build an example: cargo build --example simple_struct 4 | // 2) go to the `examples` folder: cd target/debug/examples 5 | // 3) run an example: ./simple_struct (without parameters) => entered interactive mode 6 | // ./simple_struct 30 QWE QWERTY => args: Ok(Args { age: 30, first_name: "QWE", second_name: "QWERTY" }) 7 | // To learn more about the parameters, use "help" flag: ./simple_struct --help 8 | 9 | use interactive_clap::{ResultFromCli, ToCliArgs}; 10 | 11 | #[derive(Debug, Clone, interactive_clap::InteractiveClap)] 12 | pub struct Args { 13 | age: u64, 14 | first_name: String, 15 | second_name: String, 16 | } 17 | 18 | fn main() -> color_eyre::Result<()> { 19 | let mut cli_args = Args::parse(); 20 | let context = (); // default: input_context = () 21 | loop { 22 | let args = ::from_cli(Some(cli_args), context); 23 | match args { 24 | ResultFromCli::Ok(cli_args) | ResultFromCli::Cancel(Some(cli_args)) => { 25 | println!( 26 | "Your console command: {}", 27 | shell_words::join(cli_args.to_cli_args()) 28 | ); 29 | return Ok(()); 30 | } 31 | ResultFromCli::Cancel(None) => { 32 | println!("Goodbye!"); 33 | return Ok(()); 34 | } 35 | ResultFromCli::Back => { 36 | cli_args = Default::default(); 37 | } 38 | ResultFromCli::Err(cli_args, err) => { 39 | if let Some(cli_args) = cli_args { 40 | println!( 41 | "Your console command: {}", 42 | shell_words::join(cli_args.to_cli_args()) 43 | ); 44 | } 45 | return Err(err); 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/struct_with_context.rs: -------------------------------------------------------------------------------- 1 | // This example shows additional functionality of the "interactive-clap" macro for parsing command line data into a structure using the context attributes of the macro. 2 | 3 | // 1) build an example: cargo build --example struct_with_context 4 | // 2) go to the `examples` folder: cd target/debug/examples 5 | // 3) run an example: ./struct_with_context (without parameters) => entered interactive mode 6 | // ./struct_with_context account QWERTY => offline_args: Ok(OfflineArgs { account: Sender { sender_account_id: "QWERTY" } }) 7 | // To learn more about the parameters, use "help" flag: ./struct_with_context --help 8 | 9 | use interactive_clap::{ResultFromCli, ToCliArgs}; 10 | 11 | mod simple_enum; 12 | 13 | #[derive(Debug, Clone)] 14 | pub enum ConnectionConfig { 15 | Testnet, 16 | Mainnet, 17 | Betanet, 18 | } 19 | 20 | #[derive(Debug, Clone, interactive_clap::InteractiveClap)] 21 | #[interactive_clap(input_context = ())] 22 | #[interactive_clap(output_context = OfflineArgsContext)] 23 | pub struct OfflineArgs { 24 | #[interactive_clap(named_arg)] 25 | ///Specify a sender 26 | sender: Sender, 27 | } 28 | 29 | #[derive(Debug)] 30 | pub struct OfflineArgsContext { 31 | pub some_context_field: i64, 32 | } 33 | 34 | impl OfflineArgsContext { 35 | fn from_previous_context( 36 | _previous_context: (), 37 | _scope: &::InteractiveClapContextScope, 38 | ) -> color_eyre::eyre::Result { 39 | Ok(Self { 40 | some_context_field: 42, 41 | }) 42 | } 43 | } 44 | 45 | impl From for NetworkContext { 46 | fn from(_: OfflineArgsContext) -> Self { 47 | Self { 48 | connection_config: None, 49 | } 50 | } 51 | } 52 | 53 | impl From<()> for NetworkContext { 54 | fn from(_: ()) -> Self { 55 | Self { 56 | connection_config: None, 57 | } 58 | } 59 | } 60 | 61 | impl From for () { 62 | fn from(_: NetworkContext) -> Self {} 63 | } 64 | 65 | #[derive(Debug)] 66 | pub struct NetworkContext { 67 | pub connection_config: Option, 68 | } 69 | 70 | #[derive(Debug, Clone, interactive_clap::InteractiveClap)] 71 | #[interactive_clap(context = NetworkContext)] 72 | pub struct Sender { 73 | #[interactive_clap(skip_default_input_arg)] 74 | sender_account_id: String, 75 | #[interactive_clap(subcommand)] 76 | network: simple_enum::Mode, 77 | } 78 | 79 | impl Sender { 80 | fn input_sender_account_id( 81 | context: &NetworkContext, 82 | ) -> color_eyre::eyre::Result> { 83 | println!("Let's use context: {:?}", context); 84 | match inquire::CustomType::new("What is the account ID?").prompt() { 85 | Ok(value) => Ok(Some(value)), 86 | Err( 87 | inquire::error::InquireError::OperationCanceled 88 | | inquire::error::InquireError::OperationInterrupted, 89 | ) => Ok(None), 90 | Err(err) => Err(err.into()), 91 | } 92 | } 93 | } 94 | 95 | fn main() -> color_eyre::Result<()> { 96 | let mut cli_offline_args = OfflineArgs::parse(); 97 | let context = (); // #[interactive_clap(input_context = ())] 98 | loop { 99 | let offline_args = ::from_cli( 100 | Some(cli_offline_args.clone()), 101 | context, 102 | ); 103 | match offline_args { 104 | ResultFromCli::Ok(cli_offline_args) | ResultFromCli::Cancel(Some(cli_offline_args)) => { 105 | println!( 106 | "Your console command: {}", 107 | shell_words::join(cli_offline_args.to_cli_args()) 108 | ); 109 | return Ok(()); 110 | } 111 | ResultFromCli::Cancel(None) => { 112 | println!("Goodbye!"); 113 | return Ok(()); 114 | } 115 | ResultFromCli::Back => { 116 | cli_offline_args = Default::default(); 117 | } 118 | ResultFromCli::Err(cli_offline_args, err) => { 119 | if let Some(cli_offline_args) = cli_offline_args { 120 | println!( 121 | "Your console command: {}", 122 | shell_words::join(cli_offline_args.to_cli_args()) 123 | ); 124 | } 125 | return Err(err); 126 | } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /examples/struct_with_flatten.rs: -------------------------------------------------------------------------------- 1 | // This example shows additional functionality of the "interactive-clap" macro for parsing command-line data into a structure using the macro's flatten attributes. 2 | 3 | // 1) build an example: cargo build --example struct_with_flatten 4 | // 2) go to the `examples` folder: cd target/debug/examples 5 | // 3) run an example: ./struct_with_flatten (without parameters) => entered interactive mode 6 | // ./struct_with_flatten --no-docker --no-abi --out-dir /Users/Documents/Rust --color never test.testnet offline => contract: CliContract { build_command_args: Some(CliBuildCommand { no_docker: true, no_release: false, no_abi: true, no_embed_abi: false, no_doc: false, out_dir: Some("/Users/Documents/Rust"), manifest_path: None, color: Some(Never) }), contract_account_id: Some("test.testnet"), mode: Some(Offline) } 7 | // To learn more about the parameters, use "help" flag: ./struct_with_flatten --help 8 | 9 | // Note: currently there is no automatic generation of "interactive clap::From Cli" 10 | 11 | #![allow(clippy::unit_arg, dead_code, unreachable_patterns, unused_variables)] 12 | 13 | use interactive_clap::{ResultFromCli, ToCliArgs}; 14 | 15 | #[derive(Debug, Clone, interactive_clap::InteractiveClap)] 16 | #[interactive_clap(input_context = ())] 17 | #[interactive_clap(output_context = ContractContext)] 18 | #[interactive_clap(skip_default_from_cli)] 19 | pub struct Contract { 20 | #[interactive_clap(flatten)] 21 | /// Specify a build command args: 22 | build_command_args: BuildCommand, 23 | /// What is the contract account ID? 24 | contract_account_id: String, 25 | #[interactive_clap(subcommand)] 26 | pub mode: Mode, 27 | } 28 | 29 | #[derive(Debug, Clone)] 30 | pub struct ContractContext; 31 | 32 | impl ContractContext { 33 | pub fn from_previous_context( 34 | previous_context: (), 35 | scope: &::InteractiveClapContextScope, 36 | ) -> color_eyre::eyre::Result { 37 | // Your commands 38 | Ok(Self) 39 | } 40 | } 41 | 42 | impl interactive_clap::FromCli for Contract { 43 | type FromCliContext = (); 44 | type FromCliError = color_eyre::eyre::Error; 45 | fn from_cli( 46 | optional_clap_variant: Option<::CliVariant>, 47 | context: Self::FromCliContext, 48 | ) -> interactive_clap::ResultFromCli< 49 | ::CliVariant, 50 | Self::FromCliError, 51 | > 52 | where 53 | Self: Sized + interactive_clap::ToCli, 54 | { 55 | let mut clap_variant = optional_clap_variant.unwrap_or_default(); 56 | 57 | let build_command_args = 58 | if let Some(cli_build_command_args) = &clap_variant.build_command_args { 59 | BuildCommand { 60 | no_docker: cli_build_command_args.no_docker, 61 | no_release: cli_build_command_args.no_release, 62 | no_abi: cli_build_command_args.no_abi, 63 | no_embed_abi: cli_build_command_args.no_embed_abi, 64 | no_doc: cli_build_command_args.no_doc, 65 | out_dir: cli_build_command_args.out_dir.clone(), 66 | manifest_path: cli_build_command_args.manifest_path.clone(), 67 | color: cli_build_command_args.color.clone(), 68 | env: cli_build_command_args.env.clone(), 69 | } 70 | } else { 71 | BuildCommand::default() 72 | }; 73 | 74 | if clap_variant.contract_account_id.is_none() { 75 | clap_variant.contract_account_id = match Self::input_contract_account_id(&context) { 76 | Ok(Some(contract_account_id)) => Some(contract_account_id), 77 | Ok(None) => return interactive_clap::ResultFromCli::Cancel(Some(clap_variant)), 78 | Err(err) => return interactive_clap::ResultFromCli::Err(Some(clap_variant), err), 79 | }; 80 | } 81 | let contract_account_id = clap_variant 82 | .contract_account_id 83 | .clone() 84 | .expect("Unexpected error"); 85 | 86 | let new_context_scope = InteractiveClapContextScopeForContract { 87 | build_command_args, 88 | contract_account_id, 89 | }; 90 | 91 | let _output_context = 92 | match ContractContext::from_previous_context(context, &new_context_scope) { 93 | Ok(new_context) => new_context, 94 | Err(err) => return interactive_clap::ResultFromCli::Err(Some(clap_variant), err), 95 | }; 96 | 97 | match ::from_cli(clap_variant.mode.take(), context) { 98 | interactive_clap::ResultFromCli::Ok(cli_field) => { 99 | clap_variant.mode = Some(cli_field); 100 | } 101 | interactive_clap::ResultFromCli::Cancel(option_cli_field) => { 102 | clap_variant.mode = option_cli_field; 103 | return interactive_clap::ResultFromCli::Cancel(Some(clap_variant)); 104 | } 105 | interactive_clap::ResultFromCli::Cancel(option_cli_field) => { 106 | clap_variant.mode = option_cli_field; 107 | return interactive_clap::ResultFromCli::Cancel(Some(clap_variant)); 108 | } 109 | interactive_clap::ResultFromCli::Back => { 110 | return interactive_clap::ResultFromCli::Back; 111 | } 112 | interactive_clap::ResultFromCli::Err(option_cli_field, err) => { 113 | clap_variant.mode = option_cli_field; 114 | return interactive_clap::ResultFromCli::Err(Some(clap_variant), err); 115 | } 116 | }; 117 | interactive_clap::ResultFromCli::Ok(clap_variant) 118 | } 119 | } 120 | 121 | #[derive(Debug, Default, Clone, interactive_clap::InteractiveClap)] 122 | #[interactive_clap(input_context = ())] 123 | #[interactive_clap(output_context = BuildCommandlContext)] 124 | pub struct BuildCommand { 125 | /// Build contract without SourceScan verification 126 | #[interactive_clap(long)] 127 | pub no_docker: bool, 128 | /// Build contract in debug mode, without optimizations and bigger is size 129 | #[interactive_clap(long)] 130 | pub no_release: bool, 131 | /// Do not generate ABI for the contract 132 | #[interactive_clap(long)] 133 | pub no_abi: bool, 134 | /// Do not embed the ABI in the contract binary 135 | #[interactive_clap(long)] 136 | pub no_embed_abi: bool, 137 | /// Do not include rustdocs in the embedded ABI 138 | #[interactive_clap(long)] 139 | pub no_doc: bool, 140 | /// Copy final artifacts to this directory 141 | #[interactive_clap(long)] 142 | #[interactive_clap(skip_interactive_input)] 143 | pub out_dir: Option, 144 | /// Path to the `Cargo.toml` of the contract to build 145 | #[interactive_clap(long)] 146 | #[interactive_clap(skip_interactive_input)] 147 | pub manifest_path: Option, 148 | /// Coloring: auto, always, never 149 | #[interactive_clap(long)] 150 | #[interactive_clap(value_enum)] 151 | #[interactive_clap(skip_interactive_input)] 152 | pub color: Option, 153 | 154 | // `long_vec_multiple_opt` implies `skip_interactive_input` 155 | // `long_vec_multiple_opt` implies `long` 156 | #[interactive_clap(long_vec_multiple_opt)] 157 | pub env: Vec, 158 | } 159 | 160 | #[derive(Debug, Clone)] 161 | pub struct BuildCommandlContext { 162 | build_command_args: BuildCommand, 163 | } 164 | 165 | impl BuildCommandlContext { 166 | pub fn from_previous_context( 167 | _previous_context: (), 168 | scope: &::InteractiveClapContextScope, 169 | ) -> color_eyre::eyre::Result { 170 | let build_command_args = BuildCommand { 171 | no_docker: scope.no_docker, 172 | no_release: scope.no_release, 173 | no_abi: scope.no_abi, 174 | no_embed_abi: scope.no_embed_abi, 175 | no_doc: scope.no_doc, 176 | out_dir: scope.out_dir.clone(), 177 | manifest_path: scope.manifest_path.clone(), 178 | color: scope.color.clone(), 179 | env: scope.env.clone(), 180 | }; 181 | Ok(Self { build_command_args }) 182 | } 183 | } 184 | 185 | use strum::{EnumDiscriminants, EnumIter, EnumMessage}; 186 | 187 | #[derive(Debug, EnumDiscriminants, Clone, interactive_clap::InteractiveClap)] 188 | #[strum_discriminants(derive(EnumMessage, EnumIter))] 189 | ///To construct a transaction you will need to provide information about sender (signer) and receiver accounts, and actions that needs to be performed. 190 | ///Do you want to derive some information required for transaction construction automatically querying it online? 191 | pub enum Mode { 192 | /// Prepare and, optionally, submit a new transaction with online mode 193 | #[strum_discriminants(strum(message = "Yes, I keep it simple"))] 194 | Network, 195 | /// Prepare and, optionally, submit a new transaction with offline mode 196 | #[strum_discriminants(strum( 197 | message = "No, I want to work in no-network (air-gapped) environment" 198 | ))] 199 | Offline, 200 | } 201 | 202 | use std::str::FromStr; 203 | 204 | #[derive(Debug, EnumDiscriminants, Clone, clap::ValueEnum)] 205 | #[strum_discriminants(derive(EnumMessage, EnumIter))] 206 | pub enum ColorPreference { 207 | Auto, 208 | Always, 209 | Never, 210 | } 211 | 212 | impl interactive_clap::ToCli for ColorPreference { 213 | type CliVariant = ColorPreference; 214 | } 215 | 216 | impl std::fmt::Display for ColorPreference { 217 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 218 | match self { 219 | Self::Auto => write!(f, "auto"), 220 | Self::Always => write!(f, "always"), 221 | Self::Never => write!(f, "never"), 222 | } 223 | } 224 | } 225 | 226 | impl FromStr for ColorPreference { 227 | type Err = String; 228 | 229 | fn from_str(s: &str) -> Result { 230 | match s { 231 | "auto" => Ok(default_mode()), 232 | "always" => Ok(ColorPreference::Always), 233 | "never" => Ok(ColorPreference::Never), 234 | _ => Err(format!("invalid color preference: {}", s)), 235 | } 236 | } 237 | } 238 | 239 | fn default_mode() -> ColorPreference { 240 | ColorPreference::Never 241 | } 242 | 243 | impl ColorPreference { 244 | pub fn as_str(&self) -> &str { 245 | match self { 246 | ColorPreference::Auto => "auto", 247 | ColorPreference::Always => "always", 248 | ColorPreference::Never => "never", 249 | } 250 | } 251 | } 252 | 253 | fn main() -> color_eyre::Result<()> { 254 | let mut cli_contract = Contract::parse(); 255 | let context = (); // default: input_context = () 256 | loop { 257 | let contract = 258 | ::from_cli(Some(cli_contract), context); 259 | match contract { 260 | ResultFromCli::Ok(cli_contract) | ResultFromCli::Cancel(Some(cli_contract)) => { 261 | println!("contract: {cli_contract:#?}"); 262 | println!( 263 | "Your console command: {}", 264 | shell_words::join(cli_contract.to_cli_args()) 265 | ); 266 | return Ok(()); 267 | } 268 | ResultFromCli::Cancel(None) => { 269 | println!("Goodbye!"); 270 | return Ok(()); 271 | } 272 | ResultFromCli::Back => { 273 | cli_contract = Default::default(); 274 | } 275 | ResultFromCli::Err(cli_contract, err) => { 276 | if let Some(cli_contract) = cli_contract { 277 | println!( 278 | "Your console command: {}", 279 | shell_words::join(cli_contract.to_cli_args()) 280 | ); 281 | } 282 | return Err(err); 283 | } 284 | } 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /examples/struct_with_named_arg.rs: -------------------------------------------------------------------------------- 1 | // This example shows additional functionality of the "interactive-clap" macro for parsing command-line data into a structure using the macro's named attributes. 2 | // "named_arg" is a simplified version of the subcommand, consisting of a single enum element. 3 | 4 | // 1) build an example: cargo build --example struct_with_named_arg 5 | // 2) go to the `examples` folder: cd target/debug/examples 6 | // 3) run an example: ./struct_with_named_arg (without parameters) => entered interactive mode 7 | // ./struct_with_named_arg account QWERTY => account: Ok(Account { account: Sender { sender_account_id: "QWERTY" } }) 8 | // To learn more about the parameters, use "help" flag: ./struct_with_named_arg --help 9 | 10 | use interactive_clap::{ResultFromCli, ToCliArgs}; 11 | 12 | #[derive(Debug, Clone, interactive_clap::InteractiveClap)] 13 | struct Account { 14 | #[interactive_clap(named_arg)] 15 | ///Specify a sender 16 | account: Sender, 17 | } 18 | 19 | #[derive(Debug, Clone, interactive_clap::InteractiveClap)] 20 | pub struct Sender { 21 | ///What is the sender account ID? 22 | pub sender_account_id: String, 23 | } 24 | 25 | fn main() -> color_eyre::Result<()> { 26 | let mut cli_account = Account::parse(); 27 | let context = (); // default: input_context = () 28 | loop { 29 | let account = ::from_cli(Some(cli_account), context); 30 | match account { 31 | ResultFromCli::Ok(cli_account) | ResultFromCli::Cancel(Some(cli_account)) => { 32 | println!( 33 | "Your console command: {}", 34 | shell_words::join(cli_account.to_cli_args()) 35 | ); 36 | return Ok(()); 37 | } 38 | ResultFromCli::Cancel(None) => { 39 | println!("Goodbye!"); 40 | return Ok(()); 41 | } 42 | ResultFromCli::Back => { 43 | cli_account = Default::default(); 44 | } 45 | ResultFromCli::Err(cli_account, err) => { 46 | if let Some(cli_account) = cli_account { 47 | println!( 48 | "Your console command: {}", 49 | shell_words::join(cli_account.to_cli_args()) 50 | ); 51 | } 52 | return Err(err); 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/struct_with_subargs.rs: -------------------------------------------------------------------------------- 1 | // This example shows additional functionality of the "interactive-clap" macro for parsing command-line data into a structure using the macro's subargs attributes. 2 | 3 | // 1) build an example: cargo build --example struct_with_subargs 4 | // 2) go to the `examples` folder: cd target/debug/examples 5 | // 3) run an example: ./struct_with_subargs (without parameters) => entered interactive mode 6 | // ./struct_with_subargs QWERTY 18 => account: CliAccount { social_db_folder: None, account: Some(CliSender { sender_account_id: Some("QWERTY"), age: Some(18) }) } 7 | // To learn more about the parameters, use "help" flag: ./struct_with_subargs --help 8 | 9 | use interactive_clap::{ResultFromCli, ToCliArgs}; 10 | 11 | #[derive(Debug, Clone, interactive_clap::InteractiveClap)] 12 | struct Account { 13 | /// Change SocialDb prefix 14 | /// 15 | /// It's a paraghraph, describing, this argument usage in more detail 16 | /// than just the headline 17 | #[interactive_clap(long)] 18 | #[interactive_clap(skip_interactive_input)] 19 | #[interactive_clap(verbatim_doc_comment)] 20 | social_db_folder: Option, 21 | /// Sender account 22 | #[interactive_clap(subargs)] 23 | account: Sender, 24 | } 25 | 26 | #[derive(Debug, Clone, interactive_clap::InteractiveClap)] 27 | pub struct Sender { 28 | /// What is the sender account ID? 29 | sender_account_id: String, 30 | /// How old is the sender? 31 | age: u64, 32 | } 33 | 34 | fn main() -> color_eyre::Result<()> { 35 | let mut cli_account = Account::parse(); 36 | let context = (); // default: input_context = () 37 | loop { 38 | let account = ::from_cli(Some(cli_account), context); 39 | match account { 40 | ResultFromCli::Ok(cli_account) | ResultFromCli::Cancel(Some(cli_account)) => { 41 | println!("account: {cli_account:?}"); 42 | println!( 43 | "Your console command: {}", 44 | shell_words::join(cli_account.to_cli_args()) 45 | ); 46 | return Ok(()); 47 | } 48 | ResultFromCli::Cancel(None) => { 49 | println!("Goodbye!"); 50 | return Ok(()); 51 | } 52 | ResultFromCli::Back => { 53 | cli_account = Default::default(); 54 | } 55 | ResultFromCli::Err(cli_account, err) => { 56 | if let Some(cli_account) = cli_account { 57 | println!( 58 | "Your console command: {}", 59 | shell_words::join(cli_account.to_cli_args()) 60 | ); 61 | } 62 | return Err(err); 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /examples/struct_with_subcommand.rs: -------------------------------------------------------------------------------- 1 | // This example shows additional functionality of the "interactive-clap" macro for parsing command line data into a structure using a subcommand in the macro attribute. 2 | 3 | // 1) build an example: cargo build --example struct_with_subcommand 4 | // 2) go to the `examples` folder: cd target/debug/examples 5 | // 3) run an example: ./struct_with_subcommand (without parameters) => entered interactive mode 6 | // ./struct_with_subcommand network => operation_mode: Ok(OperationMode { mode: Network }) 7 | // ./struct_with_subcommand offline => operation_mode: Ok(OperationMode { mode: Offline }) 8 | // To learn more about the parameters, use "help" flag: ./struct_with_subcommand --help 9 | 10 | use interactive_clap::{ResultFromCli, ToCliArgs}; 11 | 12 | mod simple_enum; 13 | 14 | #[derive(Debug, Clone, interactive_clap::InteractiveClap)] 15 | pub struct OperationMode { 16 | #[interactive_clap(subcommand)] 17 | pub mode: simple_enum::Mode, 18 | } 19 | 20 | fn main() -> color_eyre::Result<()> { 21 | let mut cli_operation_mode = OperationMode::parse(); 22 | let context = (); // default: input_context = () 23 | loop { 24 | let operation_mode = ::from_cli( 25 | Some(cli_operation_mode), 26 | context, 27 | ); 28 | match operation_mode { 29 | ResultFromCli::Ok(cli_operation_mode) 30 | | ResultFromCli::Cancel(Some(cli_operation_mode)) => { 31 | println!( 32 | "Your console command: {}", 33 | shell_words::join(cli_operation_mode.to_cli_args()) 34 | ); 35 | return Ok(()); 36 | } 37 | ResultFromCli::Cancel(None) => { 38 | println!("Goodbye!"); 39 | return Ok(()); 40 | } 41 | ResultFromCli::Back => { 42 | cli_operation_mode = Default::default(); 43 | } 44 | ResultFromCli::Err(cli_operation_mode, err) => { 45 | if let Some(cli_operation_mode) = cli_operation_mode { 46 | println!( 47 | "Your console command: {}", 48 | shell_words::join(cli_operation_mode.to_cli_args()) 49 | ); 50 | } 51 | return Err(err); 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /examples/to_cli_args.rs: -------------------------------------------------------------------------------- 1 | // The "to_cli_args" method of the "interactive-clap" macro is designed to form and print the cli command using the interactive mode for entering parameters. 2 | 3 | // 1) build an example: cargo build --example to_cli_args 4 | // 2) go to the `examples` folder: cd target/debug/examples 5 | // 3) run an example: ./to_cli_args (without parameters) => entered interactive mode 6 | // ./to_cli_args send => Your console command: send 7 | // ./to_cli_args display => Your console command: display 8 | // To learn more about the parameters, use "help" flag: ./to_cli_args --help 9 | 10 | use inquire::Select; 11 | use interactive_clap::{ResultFromCli, SelectVariantOrBack, ToCliArgs}; 12 | use strum::{EnumDiscriminants, EnumIter, EnumMessage, IntoEnumIterator}; 13 | 14 | #[derive(Debug, Clone)] 15 | pub enum ConnectionConfig { 16 | Testnet, 17 | Mainnet, 18 | Betanet, 19 | } 20 | 21 | #[derive(Debug, Clone, interactive_clap::InteractiveClap)] 22 | #[interactive_clap(context = ConnectionConfig)] 23 | struct OnlineArgs { 24 | /// What is the name of the network 25 | #[interactive_clap(skip_default_input_arg)] 26 | network_name: String, 27 | #[interactive_clap(subcommand)] 28 | submit: Submit, 29 | } 30 | 31 | impl OnlineArgs { 32 | fn input_network_name(_context: &ConnectionConfig) -> color_eyre::eyre::Result> { 33 | match inquire::Text::new("Input network name").prompt() { 34 | Ok(value) => Ok(Some(value)), 35 | Err( 36 | inquire::error::InquireError::OperationCanceled 37 | | inquire::error::InquireError::OperationInterrupted, 38 | ) => Ok(None), 39 | Err(err) => Err(err.into()), 40 | } 41 | } 42 | } 43 | 44 | #[derive(Debug, EnumDiscriminants, Clone, clap::Parser)] 45 | #[strum_discriminants(derive(EnumMessage, EnumIter))] 46 | pub enum Submit { 47 | #[strum_discriminants(strum(message = "I want to send the transaction to the network"))] 48 | Send(Args), 49 | #[strum_discriminants(strum( 50 | message = "I only want to print base64-encoded transaction for JSON RPC input and exit" 51 | ))] 52 | Display, 53 | } 54 | 55 | #[derive(Debug, EnumDiscriminants, Clone, clap::Parser)] 56 | pub enum CliSubmit { 57 | Send(CliArgs), 58 | Display, 59 | } 60 | 61 | impl From for CliSubmit { 62 | fn from(command: Submit) -> Self { 63 | match command { 64 | Submit::Send(args) => Self::Send(args.into()), 65 | Submit::Display => Self::Display, 66 | } 67 | } 68 | } 69 | 70 | impl interactive_clap::FromCli for Submit { 71 | type FromCliContext = ConnectionConfig; 72 | type FromCliError = color_eyre::eyre::Error; 73 | 74 | fn from_cli( 75 | optional_clap_variant: Option<::CliVariant>, 76 | context: Self::FromCliContext, 77 | ) -> ResultFromCli<::CliVariant, Self::FromCliError> 78 | where 79 | Self: Sized + interactive_clap::ToCli, 80 | { 81 | match optional_clap_variant { 82 | Some(submit) => ResultFromCli::Ok(submit), 83 | None => Self::choose_variant(context), 84 | } 85 | } 86 | } 87 | 88 | impl interactive_clap::ToCliArgs for CliSubmit { 89 | fn to_cli_args(&self) -> std::collections::VecDeque { 90 | match self { 91 | Self::Send(cli_args) => { 92 | let mut args = cli_args.to_cli_args(); 93 | args.push_front("send".to_owned()); 94 | args 95 | } 96 | Self::Display => { 97 | let mut args = std::collections::VecDeque::new(); 98 | args.push_front("display".to_owned()); 99 | args 100 | } 101 | } 102 | } 103 | } 104 | 105 | impl Submit { 106 | fn choose_variant( 107 | context: ConnectionConfig, 108 | ) -> ResultFromCli< 109 | ::CliVariant, 110 | ::FromCliError, 111 | > { 112 | match Select::new( 113 | "How would you like to proceed", 114 | SubmitDiscriminants::iter() 115 | .map(SelectVariantOrBack::Variant) 116 | .chain([SelectVariantOrBack::Back]) 117 | .collect(), 118 | ) 119 | .prompt() 120 | { 121 | Ok(SelectVariantOrBack::Variant(variant)) => ResultFromCli::Ok(match variant { 122 | SubmitDiscriminants::Send => { 123 | let cli_args = 124 | match ::from_cli(None, context) { 125 | ResultFromCli::Ok(cli_args) => cli_args, 126 | ResultFromCli::Cancel(optional_cli_args) => { 127 | return ResultFromCli::Cancel(Some(CliSubmit::Send( 128 | optional_cli_args.unwrap_or_default(), 129 | ))); 130 | } 131 | ResultFromCli::Back => return ResultFromCli::Back, 132 | ResultFromCli::Err(optional_cli_args, err) => { 133 | return ResultFromCli::Err( 134 | Some(CliSubmit::Send(optional_cli_args.unwrap_or_default())), 135 | err, 136 | ); 137 | } 138 | }; 139 | CliSubmit::Send(cli_args) 140 | } 141 | SubmitDiscriminants::Display => CliSubmit::Display, 142 | }), 143 | Ok(SelectVariantOrBack::Back) => ResultFromCli::Back, 144 | Err( 145 | inquire::error::InquireError::OperationCanceled 146 | | inquire::error::InquireError::OperationInterrupted, 147 | ) => ResultFromCli::Cancel(None), 148 | Err(err) => ResultFromCli::Err(None, err.into()), 149 | } 150 | } 151 | } 152 | impl interactive_clap::ToCli for Submit { 153 | type CliVariant = CliSubmit; 154 | } 155 | 156 | impl std::fmt::Display for SubmitDiscriminants { 157 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 158 | match self { 159 | Self::Send => write!(f, "send"), 160 | Self::Display => write!(f, "display"), 161 | } 162 | } 163 | } 164 | 165 | #[derive(Debug, Clone, interactive_clap::InteractiveClap, clap::Args)] 166 | #[interactive_clap(context = ConnectionConfig)] 167 | pub struct Args { 168 | age: u64, 169 | first_name: String, 170 | second_name: String, 171 | } 172 | 173 | fn main() -> color_eyre::Result<()> { 174 | let mut cli_online_args = OnlineArgs::parse(); 175 | let context = ConnectionConfig::Testnet; //#[interactive_clap(context = ConnectionConfig)] 176 | let cli_args = loop { 177 | match ::from_cli( 178 | Some(cli_online_args), 179 | context.clone(), 180 | ) { 181 | ResultFromCli::Ok(cli_args) => break cli_args, 182 | ResultFromCli::Cancel(Some(cli_args)) => { 183 | println!( 184 | "Your console command: {}", 185 | shell_words::join(cli_args.to_cli_args()) 186 | ); 187 | return Ok(()); 188 | } 189 | ResultFromCli::Cancel(None) => { 190 | println!("Goodbye!"); 191 | return Ok(()); 192 | } 193 | ResultFromCli::Back => { 194 | cli_online_args = Default::default(); 195 | } 196 | ResultFromCli::Err(cli_args, err) => { 197 | if let Some(cli_args) = cli_args { 198 | println!( 199 | "Your console command: {}", 200 | shell_words::join(cli_args.to_cli_args()) 201 | ); 202 | } 203 | return Err(err); 204 | } 205 | } 206 | }; 207 | println!("cli_args: {:?}", cli_args); 208 | println!( 209 | "Your console command: {}", 210 | shell_words::join(cli_args.to_cli_args()) 211 | ); 212 | Ok(()) 213 | } 214 | -------------------------------------------------------------------------------- /interactive-clap-derive/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /interactive-clap-derive/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.3.2](https://github.com/near-cli-rs/interactive-clap/compare/interactive-clap-derive-v0.3.1...interactive-clap-derive-v0.3.2) - 2025-02-11 10 | 11 | ### Added 12 | 13 | - propagate doc comments on flags and arguments to `--help/-h` + structs derive refactor (#26) 14 | 15 | ### Other 16 | 17 | - Added code style check (#29) 18 | 19 | ## [0.3.1](https://github.com/near-cli-rs/interactive-clap/compare/interactive-clap-derive-v0.3.0...interactive-clap-derive-v0.3.1) - 2024-09-18 20 | 21 | ### Added 22 | 23 | - add `long_vec_multiple_opt` attribute ([#22](https://github.com/near-cli-rs/interactive-clap/pull/22)) 24 | 25 | ## [0.3.0](https://github.com/near-cli-rs/interactive-clap/compare/interactive-clap-derive-v0.2.10...interactive-clap-derive-v0.3.0) - 2024-08-09 26 | 27 | ### Fixed 28 | - [**breaking**] Proxy `try_parse_from` to Clap's `try_parse_from` as is, instead of naive parsing of `&str` ([#21](https://github.com/near-cli-rs/interactive-clap/pull/21)) 29 | 30 | ## [0.2.10](https://github.com/near-cli-rs/interactive-clap/compare/interactive-clap-derive-v0.2.9...interactive-clap-derive-v0.2.10) - 2024-04-21 31 | 32 | ### Added 33 | - Add support for "subargs" ([#17](https://github.com/near-cli-rs/interactive-clap/pull/17)) 34 | 35 | ## [0.2.9](https://github.com/near-cli-rs/interactive-clap/compare/interactive-clap-derive-v0.2.8...interactive-clap-derive-v0.2.9) - 2024-03-25 36 | 37 | ### Added 38 | - Added support for "#[interactive_clap(flatten)]" ([#15](https://github.com/near-cli-rs/interactive-clap/pull/15)) 39 | 40 | ## [0.2.8](https://github.com/near-cli-rs/interactive-clap/compare/interactive-clap-derive-v0.2.7...interactive-clap-derive-v0.2.8) - 2024-01-15 41 | 42 | ### Added 43 | - Added possibility to process optional fields ([#13](https://github.com/near-cli-rs/interactive-clap/pull/13)) 44 | 45 | ## [0.2.7](https://github.com/near-cli-rs/interactive-clap/compare/interactive-clap-derive-v0.2.6...interactive-clap-derive-v0.2.7) - 2023-10-13 46 | 47 | ### Added 48 | - Add support for "flatten" ([#11](https://github.com/near-cli-rs/interactive-clap/pull/11)) 49 | 50 | ## [0.2.6](https://github.com/near-cli-rs/interactive-clap/compare/interactive-clap-derive-v0.2.5...interactive-clap-derive-v0.2.6) - 2023-10-05 51 | 52 | ### Fixed 53 | - named_args/unnamed_args/args_without_attrs conflict ([#9](https://github.com/near-cli-rs/interactive-clap/pull/9)) 54 | 55 | ## [0.2.5](https://github.com/near-cli-rs/interactive-clap/compare/interactive-clap-derive-v0.2.4...interactive-clap-derive-v0.2.5) - 2023-09-21 56 | 57 | ### Fixed 58 | - fixed unnamed_args/args_without_attrs conflict 59 | 60 | ### Other 61 | - added fn try_parse_from() 62 | - Merge branch 'master' of https://github.com/FroVolod/interactive-clap 63 | 64 | ## [0.2.4](https://github.com/near-cli-rs/interactive-clap/compare/interactive-clap-derive-v0.2.3...interactive-clap-derive-v0.2.4) - 2023-06-02 65 | 66 | ### Added 67 | - Add support for boolean flags (e.g. --offline) ([#6](https://github.com/near-cli-rs/interactive-clap/pull/6)) 68 | 69 | ## [0.2.3](https://github.com/near-cli-rs/interactive-clap/compare/interactive-clap-derive-v0.2.2...interactive-clap-derive-v0.2.3) - 2023-05-30 70 | 71 | ### Fixed 72 | - Trim unnecessary spaces in inquire prompts (fix it again after recent refactoring that reverted the previous fix) 73 | -------------------------------------------------------------------------------- /interactive-clap-derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "interactive-clap-derive" 3 | version = "0.3.2" 4 | authors = ["FroVolod "] 5 | edition = "2018" 6 | license = "MIT OR Apache-2.0" 7 | repository = "https://github.com/FroVolod/interactive-clap/tree/master/interactive-clap-derive" 8 | description = "Interactive mode extension crate to Command Line Arguments Parser (https://crates.io/crates/clap) (derive macros helper crate)" 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [lib] 13 | proc-macro = true 14 | 15 | [dependencies] 16 | proc-macro2 = "1.0.24" 17 | proc-macro-error = "1" 18 | quote = "1.0" 19 | syn = "1" 20 | 21 | [dev-dependencies] 22 | prettyplease = "0.1" 23 | insta = "1" 24 | syn = { version = "1", features = ["full", "extra-traits"] } 25 | 26 | [package.metadata.docs.rs] 27 | # Additional `RUSTDOCFLAGS` to set (default: []) 28 | rustdoc-args = ["--document-private-items"] 29 | 30 | [features] 31 | default = [] 32 | introspect = [] 33 | -------------------------------------------------------------------------------- /interactive-clap-derive/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /interactive-clap-derive/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /interactive-clap-derive/src/debug.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "introspect")] 2 | macro_rules! dbg_cond { 3 | ($val:expr) => { 4 | dbg!($val) 5 | }; 6 | } 7 | 8 | /// this macro under `introspect` feature can be used to debug how derive proc macros 9 | /// ([`crate::InteractiveClap`], [`crate::ToCliArgs`]) work 10 | /// 11 | /// ```bash 12 | /// # interactive-clap-derive folder 13 | /// cargo test test_doc_comments_propagate --features introspect -- --nocapture 14 | /// # from repo root 15 | /// cargo run --example struct_with_subargs --features interactive-clap-derive/introspect 16 | /// ``` 17 | #[cfg(not(feature = "introspect"))] 18 | macro_rules! dbg_cond { 19 | ($val:expr) => { 20 | #[allow(unused)] 21 | #[allow(clippy::no_effect)] 22 | $val 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/derives/interactive_clap/common_methods/choose_variant.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | use proc_macro2::Span; 4 | use proc_macro_error::abort_call_site; 5 | use quote::quote; 6 | use syn; 7 | 8 | pub fn fn_choose_variant( 9 | ast: &syn::DeriveInput, 10 | variants: &syn::punctuated::Punctuated, 11 | ) -> proc_macro2::TokenStream { 12 | dbg_cond!("entered `fn_choose_variant`"); 13 | let name = &ast.ident; 14 | let interactive_clap_attrs_context = 15 | super::interactive_clap_attrs_context::InteractiveClapAttrsContext::new(ast); 16 | let command_discriminants = syn::Ident::new(&format!("{name}Discriminants"), Span::call_site()); 17 | let cli_command = syn::Ident::new(&format!("Cli{name}"), Span::call_site()); 18 | 19 | let mut cli_variant = quote!(); 20 | let mut ast_attrs: Vec<&str> = std::vec::Vec::new(); 21 | 22 | if !ast.attrs.is_empty() { 23 | for (_index, attr) in ast.attrs.clone().into_iter().enumerate() { 24 | dbg_cond!((_index, &attr)); 25 | if attr.path.is_ident("interactive_clap") { 26 | for attr_token in attr.tokens.clone() { 27 | if let proc_macro2::TokenTree::Group(group) = attr_token { 28 | if group.stream().to_string().contains("disable_back") { 29 | ast_attrs.push("disable_back"); 30 | }; 31 | } 32 | } 33 | }; 34 | dbg_cond!(attr.path.is_ident("strum_discriminants")); 35 | if attr.path.is_ident("strum_discriminants") { 36 | for attr_token in attr.tokens.clone() { 37 | if let proc_macro2::TokenTree::Group(group) = attr_token { 38 | let group_stream_no_whitespace = group 39 | .stream() 40 | .to_string() 41 | .split_whitespace() 42 | .collect::>() 43 | .join(""); 44 | dbg_cond!(&group_stream_no_whitespace); 45 | if &group_stream_no_whitespace == "derive(EnumMessage,EnumIter)" { 46 | ast_attrs.push("strum_discriminants"); 47 | }; 48 | } 49 | } 50 | }; 51 | } 52 | dbg_cond!(&ast_attrs); 53 | if ast_attrs.contains(&"strum_discriminants") { 54 | let doc_attrs = ast 55 | .attrs 56 | .iter() 57 | .filter(|attr| attr.path.is_ident("doc")) 58 | .filter_map(|attr| { 59 | for attr_token in attr.tokens.clone() { 60 | if let proc_macro2::TokenTree::Literal(literal) = attr_token { 61 | return Some(literal); 62 | } 63 | } 64 | None 65 | }); 66 | 67 | let enum_variants = variants.iter().map(|variant| { 68 | let variant_ident = &variant.ident; 69 | match &variant.fields { 70 | syn::Fields::Unnamed(_) => { 71 | quote! { 72 | #command_discriminants::#variant_ident => { 73 | #cli_command::#variant_ident(Default::default()) 74 | } 75 | } 76 | } 77 | syn::Fields::Unit => { 78 | quote! { 79 | #command_discriminants::#variant_ident => #cli_command::#variant_ident 80 | } 81 | } 82 | _ => abort_call_site!( 83 | "Only option `Fields::Unnamed` or `Fields::Unit` is needed" 84 | ), 85 | } 86 | }); 87 | let actions_push_back = if ast_attrs.contains(&"disable_back") { 88 | quote!() 89 | } else { 90 | quote! {.chain([SelectVariantOrBack::Back])} 91 | }; 92 | 93 | cli_variant = quote! { 94 | use interactive_clap::SelectVariantOrBack; 95 | use inquire::Select; 96 | use strum::{EnumMessage, IntoEnumIterator}; 97 | 98 | let selected_variant = Select::new( 99 | concat!(#( #doc_attrs, )*).trim(), 100 | #command_discriminants::iter() 101 | .map(SelectVariantOrBack::Variant) 102 | #actions_push_back 103 | .collect(), 104 | ) 105 | .prompt(); 106 | match selected_variant { 107 | Ok(SelectVariantOrBack::Variant(variant)) => { 108 | let cli_args = match variant { 109 | #( #enum_variants, )* 110 | }; 111 | return interactive_clap::ResultFromCli::Ok(cli_args); 112 | }, 113 | Ok(SelectVariantOrBack::Back) => return interactive_clap::ResultFromCli::Back, 114 | Err( 115 | inquire::error::InquireError::OperationCanceled 116 | | inquire::error::InquireError::OperationInterrupted, 117 | ) => return interactive_clap::ResultFromCli::Cancel(None), 118 | Err(err) => return interactive_clap::ResultFromCli::Err(None, err.into()), 119 | } 120 | }; 121 | } 122 | }; 123 | let context = interactive_clap_attrs_context.get_input_context_dir(); 124 | 125 | quote! { 126 | pub fn choose_variant(context: #context) -> interactive_clap::ResultFromCli< 127 | ::CliVariant, 128 | ::FromCliError, 129 | > { 130 | #cli_variant 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/derives/interactive_clap/common_methods/fields_with_skip_default_input_arg.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | use syn; 4 | 5 | pub fn is_field_with_skip_default_input_arg(field: &syn::Field) -> bool { 6 | if field.attrs.is_empty() { 7 | return false; 8 | } 9 | field 10 | .attrs 11 | .iter() 12 | .filter(|attr| attr.path.is_ident("interactive_clap")) 13 | .flat_map(|attr| attr.tokens.clone()) 14 | .any(|attr_token| { 15 | attr_token.to_string().contains("skip_default_input_arg") 16 | || attr_token.to_string().contains("flatten") 17 | || attr_token.to_string().contains("subargs") 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/derives/interactive_clap/common_methods/from_cli_for_enum.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | use proc_macro2::Span; 4 | use proc_macro_error::abort_call_site; 5 | use quote::quote; 6 | use syn; 7 | 8 | pub fn from_cli_for_enum( 9 | ast: &syn::DeriveInput, 10 | variants: &syn::punctuated::Punctuated, 11 | ) -> proc_macro2::TokenStream { 12 | let name = &ast.ident; 13 | let cli_name = syn::Ident::new(&format!("Cli{name}"), Span::call_site()); 14 | 15 | let interactive_clap_attrs_context = 16 | super::interactive_clap_attrs_context::InteractiveClapAttrsContext::new(ast); 17 | if interactive_clap_attrs_context.is_skip_default_from_cli { 18 | return quote!(); 19 | }; 20 | 21 | let from_cli_variants = variants.iter().map(|variant| { 22 | let variant_ident = &variant.ident; 23 | 24 | let output_context = match &interactive_clap_attrs_context.output_context_dir { 25 | Some(output_context_dir) => { 26 | quote! { 27 | type Alias = <#name as interactive_clap::ToInteractiveClapContextScope>::InteractiveClapContextScope; 28 | let new_context_scope = Alias::#variant_ident; 29 | let output_context = match #output_context_dir::from_previous_context(context.clone(), &new_context_scope) { 30 | Ok(new_context) => new_context, 31 | Err(err) => return interactive_clap::ResultFromCli::Err(Some(#cli_name::#variant_ident(inner_cli_args)), err), 32 | }; 33 | } 34 | } 35 | None => { 36 | quote! { 37 | let output_context = context.clone(); 38 | } 39 | } 40 | }; 41 | 42 | match &variant.fields { 43 | syn::Fields::Unnamed(fields) => { 44 | let ty = &fields.unnamed[0].ty; 45 | quote! { 46 | Some(#cli_name::#variant_ident(inner_cli_args)) => { 47 | #output_context 48 | let cli_inner_args = <#ty as interactive_clap::FromCli>::from_cli(Some(inner_cli_args), output_context.into()); 49 | match cli_inner_args { 50 | interactive_clap::ResultFromCli::Ok(cli_args) => { 51 | interactive_clap::ResultFromCli::Ok(#cli_name::#variant_ident(cli_args)) 52 | } 53 | interactive_clap::ResultFromCli::Back => { 54 | optional_clap_variant = None; 55 | continue; 56 | }, 57 | interactive_clap::ResultFromCli::Cancel(Some(cli_args)) => { 58 | interactive_clap::ResultFromCli::Cancel(Some(#cli_name::#variant_ident(cli_args))) 59 | } 60 | interactive_clap::ResultFromCli::Cancel(None) => { 61 | interactive_clap::ResultFromCli::Cancel(None) 62 | } 63 | interactive_clap::ResultFromCli::Err(Some(cli_args), err) => { 64 | interactive_clap::ResultFromCli::Err(Some(#cli_name::#variant_ident(cli_args)), err) 65 | } 66 | interactive_clap::ResultFromCli::Err(None, err) => { 67 | interactive_clap::ResultFromCli::Err(None, err) 68 | } 69 | } 70 | } 71 | } 72 | }, 73 | syn::Fields::Unit => { 74 | match &interactive_clap_attrs_context.output_context_dir { 75 | Some(output_context_dir) => quote! { 76 | Some(#cli_name::#variant_ident) => { 77 | type Alias = <#name as interactive_clap::ToInteractiveClapContextScope>::InteractiveClapContextScope; 78 | let new_context_scope = Alias::#variant_ident; 79 | let output_context = match #output_context_dir::from_previous_context(context.clone(), &new_context_scope) { 80 | Ok(new_context) => new_context, 81 | Err(err) => return interactive_clap::ResultFromCli::Err(Some(#cli_name::#variant_ident), err), 82 | }; 83 | interactive_clap::ResultFromCli::Ok(#cli_name::#variant_ident) 84 | } 85 | }, 86 | None => quote! { 87 | Some(#cli_name::#variant_ident) => { 88 | interactive_clap::ResultFromCli::Ok(#cli_name::#variant_ident) 89 | }, 90 | } 91 | } 92 | }, 93 | _ => abort_call_site!("Only option `Fields::Unnamed` or `Fields::Unit` is needed") 94 | } 95 | }); 96 | 97 | let input_context_dir = interactive_clap_attrs_context 98 | .clone() 99 | .get_input_context_dir(); 100 | 101 | quote! { 102 | impl interactive_clap::FromCli for #name { 103 | type FromCliContext = #input_context_dir; 104 | type FromCliError = color_eyre::eyre::Error; 105 | fn from_cli( 106 | mut optional_clap_variant: Option<::CliVariant>, 107 | context: Self::FromCliContext, 108 | ) -> interactive_clap::ResultFromCli<::CliVariant, Self::FromCliError> where Self: Sized + interactive_clap::ToCli { 109 | loop { 110 | return match optional_clap_variant { 111 | #(#from_cli_variants)* 112 | None => match Self::choose_variant(context.clone()) { 113 | interactive_clap::ResultFromCli::Ok(cli_args) => { 114 | optional_clap_variant = Some(cli_args); 115 | continue; 116 | }, 117 | result => return result, 118 | }, 119 | } 120 | } 121 | } 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/derives/interactive_clap/common_methods/interactive_clap_attrs_context.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | use quote::quote; 4 | use syn; 5 | 6 | #[derive(Debug, Clone)] 7 | pub struct InteractiveClapAttrsContext { 8 | pub context_dir: Option, 9 | pub input_context_dir: Option, 10 | pub output_context_dir: Option, 11 | pub is_skip_default_from_cli: bool, 12 | } 13 | 14 | impl InteractiveClapAttrsContext { 15 | pub fn new(ast: &syn::DeriveInput) -> Self { 16 | let mut context_dir = quote!(); 17 | let mut input_context_dir = quote!(); 18 | let mut output_context_dir = quote!(); 19 | let mut is_skip_default_from_cli = false; 20 | if !ast.attrs.is_empty() { 21 | for attr in ast.attrs.clone() { 22 | if attr.path.is_ident("interactive_clap") { 23 | for attr_token in attr.tokens.clone() { 24 | if let proc_macro2::TokenTree::Group(group) = attr_token { 25 | if group.stream().to_string().contains("output_context") { 26 | let group_stream = 27 | &group.stream().into_iter().collect::>()[2..]; 28 | output_context_dir = quote! {#(#group_stream)*}; 29 | } else if group.stream().to_string().contains("input_context") { 30 | let group_stream = 31 | &group.stream().into_iter().collect::>()[2..]; 32 | input_context_dir = quote! {#(#group_stream)*}; 33 | } else if group.stream().to_string().contains("context") { 34 | let group_stream = 35 | &group.stream().into_iter().collect::>()[2..]; 36 | context_dir = quote! {#(#group_stream)*}; 37 | }; 38 | if group.stream().to_string().contains("skip_default_from_cli") { 39 | is_skip_default_from_cli = true; 40 | }; 41 | } 42 | } 43 | }; 44 | } 45 | }; 46 | let context_dir: Option = if context_dir.is_empty() { 47 | None 48 | } else { 49 | Some(context_dir) 50 | }; 51 | let input_context_dir: Option = if input_context_dir.is_empty() { 52 | None 53 | } else { 54 | Some(input_context_dir) 55 | }; 56 | let output_context_dir: Option = if output_context_dir.is_empty() 57 | { 58 | None 59 | } else { 60 | Some(output_context_dir) 61 | }; 62 | Self { 63 | context_dir, 64 | input_context_dir, 65 | output_context_dir, 66 | is_skip_default_from_cli, 67 | } 68 | } 69 | 70 | pub fn get_input_context_dir(self) -> proc_macro2::TokenStream { 71 | let context_dir = match self.context_dir { 72 | Some(context_dir) => context_dir, 73 | None => quote!(), 74 | }; 75 | if !context_dir.is_empty() { 76 | return context_dir; 77 | }; 78 | match self.input_context_dir { 79 | Some(input_context_dir) => input_context_dir, 80 | None => quote! {()}, 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/derives/interactive_clap/common_methods/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod choose_variant; 2 | pub mod fields_with_skip_default_input_arg; 3 | pub mod from_cli_for_enum; 4 | pub mod interactive_clap_attrs_context; 5 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/derives/interactive_clap/mod.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | use proc_macro2::{Span, TokenStream}; 4 | use proc_macro_error::abort_call_site; 5 | use quote::{quote, ToTokens}; 6 | use syn; 7 | 8 | /// these are common methods, reused for both the [structs] and `enums` derives 9 | pub(super) mod common_methods; 10 | 11 | fn get_names(ast: &syn::DeriveInput) -> (&syn::Ident, syn::Ident) { 12 | let name = &ast.ident; 13 | let cli_name = { 14 | let cli_name_string = format!("Cli{}", name); 15 | syn::Ident::new(&cli_name_string, Span::call_site()) 16 | }; 17 | (name, cli_name) 18 | } 19 | 20 | pub fn impl_interactive_clap(ast: &syn::DeriveInput) -> TokenStream { 21 | let (name, cli_name) = get_names(ast); 22 | match &ast.data { 23 | syn::Data::Struct(data_struct) => { 24 | self::structs::token_stream(name, &cli_name, ast, &data_struct.fields) 25 | } 26 | syn::Data::Enum(syn::DataEnum { variants, .. }) => { 27 | let enum_variants = variants.iter().map(|variant| { 28 | let ident = &variant.ident; 29 | let mut attrs: Vec = Vec::new(); 30 | if !&variant.attrs.is_empty() { 31 | for attr in &variant.attrs { 32 | if attr.path.is_ident("doc") { 33 | attrs.push(attr.into_token_stream()); 34 | }; 35 | if attr.path.is_ident("cfg") { 36 | for attr_token in attr.tokens.clone() { 37 | match attr_token { 38 | proc_macro2::TokenTree::Group(group) => { 39 | if group.stream().to_string().contains("feature") { 40 | attrs.push(attr.into_token_stream()); 41 | } else { 42 | continue; 43 | }; 44 | } 45 | _ => { 46 | abort_call_site!("Only option `TokenTree::Group` is needed") 47 | } 48 | } 49 | } 50 | }; 51 | } 52 | match &variant.fields { 53 | syn::Fields::Unnamed(fields) => { 54 | let ty = &fields.unnamed[0].ty; 55 | if attrs.is_empty() { 56 | quote! {#ident(<#ty as interactive_clap::ToCli>::CliVariant)} 57 | } else { 58 | quote! { 59 | #(#attrs)* 60 | #ident(<#ty as interactive_clap::ToCli>::CliVariant) 61 | } 62 | } 63 | } 64 | syn::Fields::Unit => { 65 | if attrs.is_empty() { 66 | quote! {#ident} 67 | } else { 68 | quote! { 69 | #(#attrs)* 70 | #ident 71 | } 72 | } 73 | } 74 | _ => abort_call_site!( 75 | "Only option `Fields::Unnamed` or `Fields::Unit` is needed" 76 | ), 77 | } 78 | } else { 79 | match &variant.fields { 80 | syn::Fields::Unnamed(fields) => { 81 | let ty = &fields.unnamed[0].ty; 82 | quote! { #ident(<#ty as interactive_clap::ToCli>::CliVariant) } 83 | } 84 | syn::Fields::Unit => { 85 | quote! { #ident } 86 | } 87 | _ => abort_call_site!( 88 | "Only option `Fields::Unnamed` or `Fields::Unit` is needed" 89 | ), 90 | } 91 | } 92 | }); 93 | let for_cli_enum_variants = variants.iter().map(|variant| { 94 | let ident = &variant.ident; 95 | match &variant.fields { 96 | syn::Fields::Unnamed(_) => { 97 | quote! { #name::#ident(arg) => Self::#ident(arg.into()) } 98 | } 99 | syn::Fields::Unit => { 100 | quote! { #name::#ident => Self::#ident } 101 | } 102 | _ => abort_call_site!( 103 | "Only option `Fields::Unnamed` or `Fields::Unit` is needed" 104 | ), 105 | } 106 | }); 107 | 108 | let scope_for_enum = context_scope_for_enum(name); 109 | 110 | let fn_choose_variant = 111 | self::common_methods::choose_variant::fn_choose_variant(ast, variants); 112 | 113 | let fn_from_cli_for_enum = 114 | self::common_methods::from_cli_for_enum::from_cli_for_enum(ast, variants); 115 | 116 | quote! { 117 | #[derive(Debug, Clone, clap::Parser, interactive_clap::ToCliArgs)] 118 | pub enum #cli_name { 119 | #( #enum_variants, )* 120 | } 121 | 122 | impl interactive_clap::ToCli for #name { 123 | type CliVariant = #cli_name; 124 | } 125 | 126 | #scope_for_enum 127 | 128 | impl From<#name> for #cli_name { 129 | fn from(command: #name) -> Self { 130 | match command { 131 | #( #for_cli_enum_variants, )* 132 | } 133 | } 134 | } 135 | 136 | #fn_from_cli_for_enum 137 | 138 | impl #name { 139 | #fn_choose_variant 140 | 141 | pub fn try_parse() -> Result<#cli_name, clap::Error> { 142 | <#cli_name as clap::Parser>::try_parse() 143 | } 144 | 145 | pub fn parse() -> #cli_name { 146 | <#cli_name as clap::Parser>::parse() 147 | } 148 | 149 | pub fn try_parse_from(itr: I) -> Result<#cli_name, clap::Error> 150 | where 151 | I: ::std::iter::IntoIterator, 152 | T: ::std::convert::Into<::std::ffi::OsString> + ::std::clone::Clone, 153 | { 154 | <#cli_name as clap::Parser>::try_parse_from(itr) 155 | } 156 | } 157 | } 158 | } 159 | _ => abort_call_site!("`#[derive(InteractiveClap)]` only supports structs and enums"), 160 | } 161 | } 162 | 163 | /** This module describes [`crate::InteractiveClap`] derive logic in case when [`syn::DeriveInput`] 164 | is a struct 165 | 166 | The structure of produced derive output is as follows, where code blocks are generated by 167 | submodules with corresponding names: 168 | 169 | ```rust,ignore 170 | quote::quote! { 171 | #to_cli_trait_block 172 | #input_args_impl_block 173 | #to_interactive_clap_context_scope_trait_block 174 | #from_cli_trait_block 175 | #clap_for_named_arg_enum_block 176 | } 177 | ``` 178 | */ 179 | pub(crate) mod structs { 180 | pub(crate) mod to_cli_trait; 181 | 182 | mod input_args_impl; 183 | 184 | mod to_interactive_clap_context_scope_trait; 185 | 186 | mod from_cli_trait; 187 | 188 | mod clap_for_named_arg_enum; 189 | 190 | /// these are common field methods, reused by other [structs](super::structs) submodules 191 | pub(super) mod common_field_methods; 192 | 193 | /// returns the whole result `TokenStream` of derive logic of containing module 194 | pub fn token_stream( 195 | name: &syn::Ident, 196 | cli_name: &syn::Ident, 197 | ast: &syn::DeriveInput, 198 | fields: &syn::Fields, 199 | ) -> proc_macro2::TokenStream { 200 | let b1 = to_cli_trait::token_stream(name, cli_name, fields); 201 | let b2 = input_args_impl::token_stream(ast, fields); 202 | let b3 = to_interactive_clap_context_scope_trait::token_stream(ast, fields); 203 | let b4 = from_cli_trait::token_stream(ast, fields); 204 | let b5 = clap_for_named_arg_enum::token_stream(ast, fields); 205 | 206 | quote::quote! { 207 | #b1 208 | #b2 209 | #b3 210 | #b4 211 | #b5 212 | } 213 | } 214 | } 215 | 216 | fn context_scope_for_enum(name: &syn::Ident) -> proc_macro2::TokenStream { 217 | let interactive_clap_context_scope_for_enum = syn::Ident::new( 218 | &format!("InteractiveClapContextScopeFor{}", &name), 219 | Span::call_site(), 220 | ); 221 | let enum_discriminants = syn::Ident::new(&format!("{}Discriminants", &name), Span::call_site()); 222 | quote! { 223 | pub type #interactive_clap_context_scope_for_enum = #enum_discriminants; 224 | impl interactive_clap::ToInteractiveClapContextScope for #name { 225 | type InteractiveClapContextScope = #interactive_clap_context_scope_for_enum; 226 | } 227 | } 228 | } 229 | 230 | #[cfg(test)] 231 | pub(crate) mod to_cli_args_structs_test_bridge { 232 | struct Opts { 233 | name: syn::Ident, 234 | cli_name: syn::Ident, 235 | input_fields: syn::Fields, 236 | } 237 | fn prepare(ast: &syn::DeriveInput) -> Opts { 238 | let (name, cli_name) = super::get_names(ast); 239 | let input_fields = match &ast.data { 240 | syn::Data::Struct(data_struct) => data_struct.fields.clone(), 241 | syn::Data::Enum(..) | syn::Data::Union(..) => { 242 | unreachable!("stuct DeriveInput expected"); 243 | } 244 | }; 245 | Opts { 246 | name: name.clone(), 247 | cli_name, 248 | input_fields, 249 | } 250 | } 251 | 252 | pub fn partial_output(ast: &syn::DeriveInput) -> syn::Result { 253 | let opts = prepare(ast); 254 | 255 | let (token_stream, _unused_byproduct) = 256 | super::structs::to_cli_trait::cli_variant_struct::token_stream( 257 | &opts.name, 258 | &opts.cli_name, 259 | &opts.input_fields, 260 | ); 261 | syn::parse2(token_stream) 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/derives/interactive_clap/structs/clap_for_named_arg_enum.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | derive of helper enum for structs with `#[interactive_clap(named_arg)]` on fields 3 | 4 | 5 | ```rust,ignore 6 | struct #name { 7 | #[interactive_clap(named_arg)] 8 | ///Specify a sender 9 | field_name: Sender, 10 | } 11 | ``` 12 | 13 | gets transformed 14 | => 15 | 16 | ```rust,ignore 17 | #[derive(Debug, Clone, clap::Parser, interactive_clap_derive::ToCliArgs)] 18 | pub enum ClapNamedArgSenderFor#name { 19 | ///Specify a sender 20 | FieldName(::CliVariant), 21 | } 22 | impl From for ClapNamedArgSenderFor#name { 23 | fn from(item: Sender) -> Self { 24 | Self::FieldName(::CliVariant::from(item)) 25 | } 26 | } 27 | ``` 28 | */ 29 | use proc_macro2::Span; 30 | use proc_macro_error::abort_call_site; 31 | use quote::{quote, ToTokens}; 32 | use syn; 33 | 34 | /// returns the whole result `TokenStream` of derive logic of containing module 35 | pub fn token_stream(ast: &syn::DeriveInput, fields: &syn::Fields) -> proc_macro2::TokenStream { 36 | let name = &ast.ident; 37 | fields 38 | .iter() 39 | .find_map(|field| field_transform(name, field)) 40 | .unwrap_or(quote!()) 41 | } 42 | 43 | fn field_transform(name: &syn::Ident, field: &syn::Field) -> Option { 44 | let ident_field = &field.clone().ident.expect("this field does not exist"); 45 | let variant_name_string = 46 | crate::helpers::snake_case_to_camel_case::snake_case_to_camel_case(ident_field.to_string()); 47 | let variant_name = &syn::Ident::new(&variant_name_string, Span::call_site()); 48 | let attr_doc_vec: Vec<_> = field 49 | .attrs 50 | .iter() 51 | .filter(|attr| attr.path.is_ident("doc")) 52 | .map(|attr| attr.into_token_stream()) 53 | .collect(); 54 | field.attrs.iter() 55 | .filter(|attr| attr.path.is_ident("interactive_clap")) 56 | .flat_map(|attr| attr.tokens.clone()) 57 | .filter(|attr_token| { 58 | match attr_token { 59 | proc_macro2::TokenTree::Group(group) => group.stream().to_string() == *"named_arg", 60 | _ => abort_call_site!("Only option `TokenTree::Group` is needed") 61 | } 62 | }) 63 | .map(|_| { 64 | let ty = &field.ty; 65 | let type_string = match ty { 66 | syn::Type::Path(type_path) => { 67 | match type_path.path.segments.last() { 68 | Some(path_segment) => path_segment.ident.to_string(), 69 | _ => String::new() 70 | } 71 | }, 72 | _ => String::new() 73 | }; 74 | let enum_for_clap_named_arg = syn::Ident::new(&format!("ClapNamedArg{}For{}", &type_string, &name), Span::call_site()); 75 | quote! { 76 | #[derive(Debug, Clone, clap::Parser, interactive_clap_derive::ToCliArgs)] 77 | pub enum #enum_for_clap_named_arg { 78 | #(#attr_doc_vec)* 79 | #variant_name(<#ty as interactive_clap::ToCli>::CliVariant) 80 | } 81 | 82 | impl From<#ty> for #enum_for_clap_named_arg { 83 | fn from(item: #ty) -> Self { 84 | Self::#variant_name(<#ty as interactive_clap::ToCli>::CliVariant::from(item)) 85 | } 86 | } 87 | } 88 | }) 89 | .next() 90 | } 91 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/derives/interactive_clap/structs/common_field_methods/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod with_skip_interactive_input; 2 | pub mod with_subargs; 3 | pub mod with_subcommand; 4 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/derives/interactive_clap/structs/common_field_methods/with_skip_interactive_input.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | use syn; 4 | 5 | use crate::LONG_VEC_MUTLIPLE_OPT; 6 | 7 | pub fn predicate(field: &syn::Field) -> bool { 8 | field 9 | .attrs 10 | .iter() 11 | .filter(|attr| attr.path.is_ident("interactive_clap")) 12 | .flat_map(|attr| attr.tokens.clone()) 13 | .any(|attr_token| match attr_token { 14 | proc_macro2::TokenTree::Group(group) => { 15 | let group_string = group.stream().to_string(); 16 | group_string.contains("skip_interactive_input") 17 | || group_string.contains(LONG_VEC_MUTLIPLE_OPT) 18 | } 19 | _ => false, 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/derives/interactive_clap/structs/common_field_methods/with_subargs.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | use syn; 4 | 5 | pub fn predicate(field: &syn::Field) -> bool { 6 | if field.attrs.is_empty() { 7 | return false; 8 | } 9 | field 10 | .attrs 11 | .iter() 12 | .flat_map(|attr| attr.tokens.clone()) 13 | .any(|attr_token| attr_token.to_string().contains("subargs")) 14 | } 15 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/derives/interactive_clap/structs/common_field_methods/with_subcommand.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | use syn; 4 | 5 | /// This function selects fields with: subcommand, named_arg 6 | pub fn predicate(field: &syn::Field) -> bool { 7 | if field.attrs.is_empty() { 8 | return false; 9 | } 10 | field 11 | .attrs 12 | .iter() 13 | .flat_map(|attr| attr.tokens.clone()) 14 | .any(|attr_token| { 15 | attr_token.to_string().contains("named_arg") 16 | || attr_token.to_string().contains("subcommand") 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/derives/interactive_clap/structs/from_cli_trait.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | `interactive_clap::FromCli` derive 3 | 4 | This modules describes derive of `interactive_clap::FromCli` trait for `#name` struct, 5 | which happens during derive of [`crate::InteractiveClap`] for `#name` struct: 6 | 7 | The implementation combines usages of all of [super::to_cli_trait], [super::input_args_impl], 8 | [super::to_interactive_clap_context_scope_trait] 9 | 10 | 11 | derive input `#name` 12 | 13 | ```rust,ignore 14 | struct #name { 15 | age: u64, 16 | first_name: String, 17 | } 18 | ``` 19 | 20 | gets transformed 21 | => 22 | 23 | ```rust,ignore 24 | impl interactive_clap::FromCli for #name { 25 | type FromCliContext = (); 26 | type FromCliError = color_eyre::eyre::Error; 27 | fn from_cli( 28 | optional_clap_variant: Option<::CliVariant>, 29 | context: Self::FromCliContext, 30 | ) -> interactive_clap::ResultFromCli< 31 | ::CliVariant, 32 | Self::FromCliError, 33 | > 34 | where 35 | Self: Sized + interactive_clap::ToCli, 36 | { 37 | let mut clap_variant = optional_clap_variant.clone().unwrap_or_default(); 38 | if clap_variant.age.is_none() { 39 | clap_variant 40 | .age = match Self::input_age(&context) { 41 | Ok(Some(age)) => Some(age), 42 | Ok(None) => { 43 | return interactive_clap::ResultFromCli::Cancel(Some(clap_variant)); 44 | } 45 | Err(err) => { 46 | return interactive_clap::ResultFromCli::Err(Some(clap_variant), err); 47 | } 48 | }; 49 | } 50 | let age = clap_variant.age.clone().expect("Unexpected error"); 51 | if clap_variant.first_name.is_none() { 52 | clap_variant 53 | .first_name = match Self::input_first_name(&context) { 54 | Ok(Some(first_name)) => Some(first_name), 55 | Ok(None) => { 56 | return interactive_clap::ResultFromCli::Cancel(Some(clap_variant)); 57 | } 58 | Err(err) => { 59 | return interactive_clap::ResultFromCli::Err(Some(clap_variant), err); 60 | } 61 | }; 62 | } 63 | let first_name = clap_variant.first_name.clone().expect("Unexpected error"); 64 | let new_context_scope = InteractiveClapContextScopeFor#name { 65 | age: age.into(), 66 | first_name: first_name.into(), 67 | }; 68 | interactive_clap::ResultFromCli::Ok(clap_variant) 69 | } 70 | } 71 | ``` 72 | */ 73 | use proc_macro2::Span; 74 | use proc_macro_error::abort_call_site; 75 | use quote::{quote, ToTokens}; 76 | use syn; 77 | 78 | use super::common_field_methods as field_methods; 79 | use crate::derives::interactive_clap::common_methods; 80 | 81 | /// returns the whole result `TokenStream` of derive logic of containing module 82 | pub fn token_stream(ast: &syn::DeriveInput, fields: &syn::Fields) -> proc_macro2::TokenStream { 83 | let name = &ast.ident; 84 | 85 | let interactive_clap_attrs_context = 86 | common_methods::interactive_clap_attrs_context::InteractiveClapAttrsContext::new(ast); 87 | if interactive_clap_attrs_context.is_skip_default_from_cli { 88 | return quote!(); 89 | }; 90 | 91 | let fields_without_subcommand_and_subargs = fields 92 | .iter() 93 | .filter(|field| { 94 | !field_methods::with_subcommand::predicate(field) 95 | && !field_methods::with_subargs::predicate(field) 96 | }) 97 | .map(|field| { 98 | let ident_field = &field.clone().ident.expect("this field does not exist"); 99 | quote! {#ident_field: #ident_field.into()} 100 | }) 101 | .collect::>(); 102 | 103 | let fields_value = fields 104 | .iter() 105 | .map(fields_value) 106 | .filter(|token_stream| !token_stream.is_empty()); 107 | 108 | let field_value_named_arg = fields 109 | .iter() 110 | .map(|field| field_value_named_arg(name, field)) 111 | .find(|token_stream| !token_stream.is_empty()) 112 | .unwrap_or(quote!()); 113 | 114 | let field_value_subcommand = fields 115 | .iter() 116 | .map(field_value_subcommand) 117 | .find(|token_stream| !token_stream.is_empty()) 118 | .unwrap_or(quote!()); 119 | 120 | let field_value_subargs = fields 121 | .iter() 122 | .map(field_value_subargs) 123 | .find(|token_stream| !token_stream.is_empty()) 124 | .unwrap_or(quote!()); 125 | 126 | let input_context_dir = interactive_clap_attrs_context 127 | .clone() 128 | .get_input_context_dir(); 129 | 130 | let interactive_clap_context_scope_for_struct = syn::Ident::new( 131 | &format!("InteractiveClapContextScopeFor{}", &name), 132 | Span::call_site(), 133 | ); 134 | let new_context_scope = quote! { 135 | let new_context_scope = #interactive_clap_context_scope_for_struct { #(#fields_without_subcommand_and_subargs,)* }; 136 | }; 137 | 138 | let output_context = match &interactive_clap_attrs_context.output_context_dir { 139 | Some(output_context_dir) => { 140 | quote! { 141 | let output_context = match #output_context_dir::from_previous_context(context.clone(), &new_context_scope) { 142 | Ok(new_context) => new_context, 143 | Err(err) => return interactive_clap::ResultFromCli::Err(Some(clap_variant), err), 144 | }; 145 | let context = output_context; 146 | } 147 | } 148 | None => quote!(), 149 | }; 150 | 151 | quote! { 152 | impl interactive_clap::FromCli for #name { 153 | type FromCliContext = #input_context_dir; 154 | type FromCliError = color_eyre::eyre::Error; 155 | fn from_cli( 156 | optional_clap_variant: Option<::CliVariant>, 157 | context: Self::FromCliContext, 158 | ) -> interactive_clap::ResultFromCli<::CliVariant, Self::FromCliError> where Self: Sized + interactive_clap::ToCli { 159 | let mut clap_variant = optional_clap_variant.clone().unwrap_or_default(); 160 | #(#fields_value)* 161 | #new_context_scope 162 | #output_context 163 | #field_value_subargs 164 | #field_value_named_arg 165 | #field_value_subcommand; 166 | interactive_clap::ResultFromCli::Ok(clap_variant) 167 | } 168 | } 169 | } 170 | } 171 | 172 | fn fields_value(field: &syn::Field) -> proc_macro2::TokenStream { 173 | let ident_field = &field.clone().ident.expect("this field does not exist"); 174 | let fn_input_arg = syn::Ident::new(&format!("input_{}", &ident_field), Span::call_site()); 175 | if field.ty.to_token_stream().to_string() == "bool" 176 | || field_methods::with_skip_interactive_input::predicate(field) 177 | { 178 | quote! { 179 | let #ident_field = clap_variant.#ident_field.clone(); 180 | } 181 | } else if field 182 | .ty 183 | .to_token_stream() 184 | .to_string() 185 | .starts_with("Option <") 186 | { 187 | quote! { 188 | if clap_variant.#ident_field.is_none() { 189 | clap_variant 190 | .#ident_field = match Self::#fn_input_arg(&context) { 191 | Ok(optional_field) => optional_field, 192 | Err(err) => return interactive_clap::ResultFromCli::Err(Some(clap_variant), err), 193 | }; 194 | }; 195 | let #ident_field = clap_variant.#ident_field.clone(); 196 | } 197 | } else if !field_methods::with_subcommand::predicate(field) 198 | && !field_methods::with_subargs::predicate(field) 199 | { 200 | quote! { 201 | if clap_variant.#ident_field.is_none() { 202 | clap_variant 203 | .#ident_field = match Self::#fn_input_arg(&context) { 204 | Ok(Some(#ident_field)) => Some(#ident_field), 205 | Ok(None) => return interactive_clap::ResultFromCli::Cancel(Some(clap_variant)), 206 | Err(err) => return interactive_clap::ResultFromCli::Err(Some(clap_variant), err), 207 | }; 208 | }; 209 | let #ident_field = clap_variant.#ident_field.clone().expect("Unexpected error"); 210 | } 211 | } else { 212 | quote!() 213 | } 214 | } 215 | 216 | fn field_value_named_arg(name: &syn::Ident, field: &syn::Field) -> proc_macro2::TokenStream { 217 | let ident_field = &field.clone().ident.expect("this field does not exist"); 218 | let ty = &field.ty; 219 | if field.attrs.is_empty() { 220 | quote!() 221 | } else { 222 | field.attrs.iter() 223 | .filter(|attr| attr.path.is_ident("interactive_clap")) 224 | .flat_map(|attr| attr.tokens.clone()) 225 | .filter(|attr_token| { 226 | match attr_token { 227 | proc_macro2::TokenTree::Group(group) => group.stream().to_string() == *"named_arg", 228 | _ => abort_call_site!("Only option `TokenTree::Group` is needed") 229 | } 230 | }) 231 | .map(|_| { 232 | let type_string = match ty { 233 | syn::Type::Path(type_path) => { 234 | match type_path.path.segments.last() { 235 | Some(path_segment) => path_segment.ident.to_string(), 236 | _ => String::new() 237 | } 238 | }, 239 | _ => String::new() 240 | }; 241 | let enum_for_clap_named_arg = syn::Ident::new(&format!("ClapNamedArg{}For{}", &type_string, &name), Span::call_site()); 242 | let variant_name_string = crate::helpers::snake_case_to_camel_case::snake_case_to_camel_case(ident_field.to_string()); 243 | let variant_name = &syn::Ident::new(&variant_name_string, Span::call_site()); 244 | quote! { 245 | let optional_field = match clap_variant.#ident_field.take() { 246 | Some(#enum_for_clap_named_arg::#variant_name(cli_arg)) => Some(cli_arg), 247 | None => None, 248 | }; 249 | match <#ty as interactive_clap::FromCli>::from_cli( 250 | optional_field, 251 | context.into(), 252 | ) { 253 | interactive_clap::ResultFromCli::Ok(cli_field) => { 254 | clap_variant.#ident_field = Some(#enum_for_clap_named_arg::#variant_name(cli_field)); 255 | } 256 | interactive_clap::ResultFromCli::Cancel(optional_cli_field) => { 257 | clap_variant.#ident_field = optional_cli_field.map(#enum_for_clap_named_arg::#variant_name); 258 | return interactive_clap::ResultFromCli::Cancel(Some(clap_variant)); 259 | } 260 | interactive_clap::ResultFromCli::Back => return interactive_clap::ResultFromCli::Back, 261 | interactive_clap::ResultFromCli::Err(optional_cli_field, err) => { 262 | clap_variant.#ident_field = optional_cli_field.map(#enum_for_clap_named_arg::#variant_name); 263 | return interactive_clap::ResultFromCli::Err(Some(clap_variant), err); 264 | } 265 | } 266 | } 267 | }) 268 | .next() 269 | .unwrap_or(quote!()) 270 | } 271 | } 272 | 273 | fn field_value_subcommand(field: &syn::Field) -> proc_macro2::TokenStream { 274 | let ident_field = &field.clone().ident.expect("this field does not exist"); 275 | let ty = &field.ty; 276 | if field.attrs.is_empty() { 277 | quote!() 278 | } else { 279 | field.attrs.iter() 280 | .filter(|attr| attr.path.is_ident("interactive_clap")) 281 | .flat_map(|attr| attr.tokens.clone()) 282 | .filter(|attr_token| { 283 | match attr_token { 284 | proc_macro2::TokenTree::Group(group) => group.stream().to_string().contains("subcommand"), 285 | _ => abort_call_site!("Only option `TokenTree::Group` is needed") 286 | } 287 | }) 288 | .map(|_| { 289 | quote! { 290 | match <#ty as interactive_clap::FromCli>::from_cli(clap_variant.#ident_field.take(), context.into()) { 291 | interactive_clap::ResultFromCli::Ok(cli_field) => { 292 | clap_variant.#ident_field = Some(cli_field); 293 | } 294 | interactive_clap::ResultFromCli::Cancel(option_cli_field) => { 295 | clap_variant.#ident_field = option_cli_field; 296 | return interactive_clap::ResultFromCli::Cancel(Some(clap_variant)); 297 | } 298 | interactive_clap::ResultFromCli::Cancel(option_cli_field) => { 299 | clap_variant.#ident_field = option_cli_field; 300 | return interactive_clap::ResultFromCli::Cancel(Some(clap_variant)); 301 | } 302 | interactive_clap::ResultFromCli::Back => return interactive_clap::ResultFromCli::Back, 303 | interactive_clap::ResultFromCli::Err(option_cli_field, err) => { 304 | clap_variant.#ident_field = option_cli_field; 305 | return interactive_clap::ResultFromCli::Err(Some(clap_variant), err); 306 | } 307 | } 308 | } 309 | }) 310 | .next() 311 | .unwrap_or(quote!()) 312 | } 313 | } 314 | 315 | fn field_value_subargs(field: &syn::Field) -> proc_macro2::TokenStream { 316 | let ident_field = &field.clone().ident.expect("this field does not exist"); 317 | let ty = &field.ty; 318 | if field.attrs.is_empty() { 319 | quote!() 320 | } else { 321 | field.attrs.iter() 322 | .filter(|attr| attr.path.is_ident("interactive_clap")) 323 | .flat_map(|attr| attr.tokens.clone()) 324 | .filter(|attr_token| { 325 | match attr_token { 326 | proc_macro2::TokenTree::Group(group) => group.stream().to_string().contains("subargs"), 327 | _ => abort_call_site!("Only option `TokenTree::Group` is needed") 328 | } 329 | }) 330 | .map(|_| { 331 | quote! { 332 | match #ty::from_cli( 333 | optional_clap_variant.unwrap_or_default().#ident_field, 334 | context.into(), 335 | ) { 336 | interactive_clap::ResultFromCli::Ok(cli_field) => clap_variant.#ident_field = Some(cli_field), 337 | interactive_clap::ResultFromCli::Cancel(optional_cli_field) => { 338 | clap_variant.#ident_field = optional_cli_field; 339 | return interactive_clap::ResultFromCli::Cancel(Some(clap_variant)); 340 | } 341 | interactive_clap::ResultFromCli::Back => return interactive_clap::ResultFromCli::Back, 342 | interactive_clap::ResultFromCli::Err(optional_cli_field, err) => { 343 | clap_variant.#ident_field = optional_cli_field; 344 | return interactive_clap::ResultFromCli::Err(Some(clap_variant), err); 345 | } 346 | }; 347 | } 348 | }) 349 | .next() 350 | .unwrap_or(quote!()) 351 | } 352 | } 353 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/derives/interactive_clap/structs/input_args_impl.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | per-field input with [inquire::CustomType](https://docs.rs/inquire/0.6.2/inquire/struct.CustomType.html) impl block 3 | 4 | This modules describes derive of input args implementation block for `#name` struct, 5 | which contains functions `input_#field_ident` per each field, 6 | which prompt for value of each field via [inquire::CustomType](https://docs.rs/inquire/0.6.2/inquire/struct.CustomType.html) 7 | , which happens during derive of [`crate::InteractiveClap`] for `#name` struct: 8 | 9 | derive input `#name` 10 | 11 | ```rust,ignore 12 | struct #name { 13 | age: u64, 14 | first_name: String, 15 | } 16 | ``` 17 | 18 | 19 | gets transformed 20 | => 21 | 22 | ```rust,ignore 23 | impl #name { 24 | fn input_age(_context: &()) -> color_eyre::eyre::Result> { 25 | match inquire::CustomType::new("age").prompt() { 26 | Ok(value) => Ok(Some(value)), 27 | Err( 28 | inquire::error::InquireError::OperationCanceled 29 | | inquire::error::InquireError::OperationInterrupted, 30 | ) => Ok(None), 31 | Err(err) => Err(err.into()), 32 | } 33 | } 34 | fn input_first_name(_context: &()) -> color_eyre::eyre::Result> { 35 | match inquire::CustomType::new("first_name").prompt() { 36 | Ok(value) => Ok(Some(value)), 37 | Err( 38 | inquire::error::InquireError::OperationCanceled 39 | | inquire::error::InquireError::OperationInterrupted, 40 | ) => Ok(None), 41 | Err(err) => Err(err.into()), 42 | } 43 | } 44 | } 45 | ``` 46 | */ 47 | extern crate proc_macro; 48 | 49 | use proc_macro2::Span; 50 | use quote::quote; 51 | use syn; 52 | 53 | use super::common_field_methods as field_methods; 54 | use crate::derives::interactive_clap::common_methods; 55 | 56 | /// returns the whole result `TokenStream` of derive logic of containing module 57 | pub fn token_stream(ast: &syn::DeriveInput, fields: &syn::Fields) -> proc_macro2::TokenStream { 58 | let name = &ast.ident; 59 | let vec_fn_input_arg = vec_fn_input_arg(ast, fields); 60 | quote! { 61 | impl #name { 62 | #(#vec_fn_input_arg)* 63 | } 64 | } 65 | } 66 | 67 | fn vec_fn_input_arg(ast: &syn::DeriveInput, fields: &syn::Fields) -> Vec { 68 | let interactive_clap_attrs_context = 69 | common_methods::interactive_clap_attrs_context::InteractiveClapAttrsContext::new(ast); 70 | let vec_fn_input_arg = fields 71 | .iter() 72 | .filter(|field| !field_methods::with_subcommand::predicate(field)) 73 | .filter(|field| { 74 | !common_methods::fields_with_skip_default_input_arg::is_field_with_skip_default_input_arg( 75 | field, 76 | ) 77 | }) 78 | .map(|field| { 79 | let ident_field = &field.clone().ident.expect("this field does not exist"); 80 | let ty = &field.ty; 81 | 82 | let input_context_dir = interactive_clap_attrs_context 83 | .clone() 84 | .get_input_context_dir(); 85 | 86 | let fn_input_arg = 87 | syn::Ident::new(&format!("input_{}", &ident_field), Span::call_site()); 88 | 89 | if field.attrs.is_empty() { 90 | let promt = &syn::LitStr::new(&ident_field.to_string(), Span::call_site()); 91 | return quote! { 92 | fn #fn_input_arg( 93 | _context: &#input_context_dir, 94 | ) -> color_eyre::eyre::Result> { 95 | match inquire::CustomType::new(#promt).prompt() { 96 | Ok(value) => Ok(Some(value)), 97 | Err(inquire::error::InquireError::OperationCanceled | inquire::error::InquireError::OperationInterrupted) => Ok(None), 98 | Err(err) => Err(err.into()), 99 | } 100 | } 101 | }; 102 | } 103 | 104 | if field_methods::with_skip_interactive_input::predicate(field) { 105 | return quote! {}; 106 | } 107 | 108 | let doc_attrs = field 109 | .attrs 110 | .iter() 111 | .filter(|attr| attr.path.is_ident("doc")) 112 | .filter_map(|attr| { 113 | for attr_token in attr.tokens.clone() { 114 | if let proc_macro2::TokenTree::Literal(literal) = attr_token { 115 | return Some(literal); 116 | } 117 | } 118 | None 119 | }); 120 | 121 | quote! { 122 | fn #fn_input_arg( 123 | _context: &#input_context_dir, 124 | ) -> color_eyre::eyre::Result> { 125 | match inquire::CustomType::new(concat!(#( #doc_attrs, )*).trim()).prompt() { 126 | Ok(value) => Ok(Some(value)), 127 | Err(inquire::error::InquireError::OperationCanceled | inquire::error::InquireError::OperationInterrupted) => Ok(None), 128 | Err(err) => Err(err.into()), 129 | } 130 | } 131 | } 132 | }) 133 | .filter(|token_stream| !token_stream.is_empty()) 134 | .collect::>(); 135 | vec_fn_input_arg 136 | } 137 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/derives/interactive_clap/structs/to_cli_trait/clap_parser_trait_adapter.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | 4 | /// returns the whole result `TokenStream` of derive logic of containing module 5 | pub fn token_stream(name: &syn::Ident, cli_name: &syn::Ident) -> TokenStream { 6 | quote! { 7 | 8 | impl #name { 9 | pub fn try_parse() -> Result<#cli_name, clap::Error> { 10 | <#cli_name as clap::Parser>::try_parse() 11 | } 12 | 13 | pub fn parse() -> #cli_name { 14 | <#cli_name as clap::Parser>::parse() 15 | } 16 | 17 | pub fn try_parse_from(itr: I) -> Result<#cli_name, clap::Error> 18 | where 19 | I: ::std::iter::IntoIterator, 20 | T: ::std::convert::Into<::std::ffi::OsString> + ::std::clone::Clone, 21 | { 22 | <#cli_name as clap::Parser>::try_parse_from(itr) 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/derives/interactive_clap/structs/to_cli_trait/cli_variant_struct/field.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | use proc_macro_error::abort_call_site; 4 | use quote::quote; 5 | use syn; 6 | 7 | pub fn field_type(ty: &syn::Type) -> syn::Type { 8 | let token_stream = match &ty { 9 | syn::Type::Path(type_path) => match type_path.path.segments.first() { 10 | Some(path_segment) => { 11 | if path_segment.ident == "Option" { 12 | match &path_segment.arguments { 13 | syn::PathArguments::AngleBracketed(gen_args) => { 14 | let ty_option = &gen_args.args; 15 | quote! { 16 | Option<<#ty_option as interactive_clap::ToCli>::CliVariant> 17 | } 18 | } 19 | _ => { 20 | quote! { 21 | Option<<#ty as interactive_clap::ToCli>::CliVariant> 22 | } 23 | } 24 | } 25 | } else if path_segment.ident == "bool" { 26 | quote! { 27 | bool 28 | } 29 | } else { 30 | quote! { 31 | Option<<#ty as interactive_clap::ToCli>::CliVariant> 32 | } 33 | } 34 | } 35 | _ => abort_call_site!("Only option `PathSegment` is needed"), 36 | }, 37 | _ => abort_call_site!("Only option `Type::Path` is needed"), 38 | }; 39 | syn::parse2(token_stream).unwrap() 40 | } 41 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/derives/interactive_clap/structs/to_cli_trait/cli_variant_struct/mod.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Span, TokenStream}; 2 | use proc_macro_error::abort_call_site; 3 | use quote::{quote, ToTokens}; 4 | 5 | use crate::{LONG_VEC_MUTLIPLE_OPT, VERBATIM_DOC_COMMENT}; 6 | 7 | /// describes derive of individual field of `#cli_name` struct 8 | /// based on transformation of input field from `#name` struct 9 | mod field; 10 | 11 | /// returns the whole result `TokenStream` of derive logic of containing module 12 | /// and additional info as second returned tuple's element, needed for another derive 13 | pub fn token_stream( 14 | name: &syn::Ident, 15 | cli_name: &syn::Ident, 16 | input_fields: &syn::Fields, 17 | ) -> (TokenStream, Vec) { 18 | let (cli_fields, ident_skip_field_vec) = fields(input_fields, name); 19 | 20 | let token_stream = quote! { 21 | #[derive(Debug, Default, Clone, clap::Parser, interactive_clap::ToCliArgs)] 22 | #[clap(author, version, about, long_about = None)] 23 | pub struct #cli_name { 24 | #( #cli_fields, )* 25 | } 26 | 27 | }; 28 | (token_stream, ident_skip_field_vec) 29 | } 30 | 31 | /// describes derive of all fields of `#cli_name` struct 32 | /// based on transformation of input fields from `#name` struct 33 | fn fields(fields: &syn::Fields, name: &syn::Ident) -> (Vec, Vec) { 34 | let mut ident_skip_field_vec: Vec = Vec::new(); 35 | 36 | let fields = fields 37 | .iter() 38 | .map(|field| { 39 | let ident_field = field.ident.clone().expect("this field does not exist"); 40 | let ty = &field.ty; 41 | let cli_ty = self::field::field_type(ty); 42 | let mut cli_field = quote! { 43 | pub #ident_field: #cli_ty 44 | }; 45 | if field.attrs.is_empty() { 46 | return cli_field; 47 | }; 48 | let mut clap_attr_vec: Vec = Vec::new(); 49 | let mut cfg_attr_vec: Vec = Vec::new(); 50 | let mut doc_attr_vec: Vec = Vec::new(); 51 | for attr in &field.attrs { 52 | dbg_cond!(attr.path.to_token_stream().into_iter().collect::>()); 53 | if attr.path.is_ident("interactive_clap") || attr.path.is_ident("cfg") { 54 | for (_index, attr_token) in attr.tokens.clone().into_iter().enumerate() { 55 | dbg_cond!((_index, &attr_token)); 56 | match attr_token { 57 | proc_macro2::TokenTree::Group(group) => { 58 | let group_string = group.stream().to_string(); 59 | if group_string.contains("subcommand") 60 | || group_string.contains("value_enum") 61 | || group_string.contains("long") 62 | || (group_string == *"skip") 63 | || (group_string == *"flatten") 64 | || (group_string == VERBATIM_DOC_COMMENT) 65 | { 66 | if group_string != LONG_VEC_MUTLIPLE_OPT { 67 | clap_attr_vec.push(group.stream()) 68 | } 69 | } else if group.stream().to_string() == *"named_arg" { 70 | let ident_subcommand = 71 | syn::Ident::new("subcommand", Span::call_site()); 72 | clap_attr_vec.push(quote! {#ident_subcommand}); 73 | let type_string = match ty { 74 | syn::Type::Path(type_path) => { 75 | match type_path.path.segments.last() { 76 | Some(path_segment) => { 77 | path_segment.ident.to_string() 78 | } 79 | _ => String::new(), 80 | } 81 | } 82 | _ => String::new(), 83 | }; 84 | let enum_for_clap_named_arg = syn::Ident::new( 85 | &format!( 86 | "ClapNamedArg{}For{}", 87 | &type_string, &name 88 | ), 89 | Span::call_site(), 90 | ); 91 | cli_field = quote! { 92 | pub #ident_field: Option<#enum_for_clap_named_arg> 93 | } 94 | }; 95 | if group.stream().to_string().contains("feature") { 96 | cfg_attr_vec.push(attr.into_token_stream()) 97 | }; 98 | if group.stream().to_string().contains("subargs") { 99 | let ident_subargs = 100 | syn::Ident::new("flatten", Span::call_site()); 101 | clap_attr_vec.push(quote! {#ident_subargs}); 102 | }; 103 | if group.stream().to_string() == *"skip" { 104 | ident_skip_field_vec.push(ident_field.clone()); 105 | cli_field = quote!() 106 | }; 107 | if group.stream().to_string() == LONG_VEC_MUTLIPLE_OPT { 108 | if !crate::helpers::type_starts_with_vec(ty) { 109 | abort_call_site!("`{}` attribute is only supposed to be used with `Vec` types", LONG_VEC_MUTLIPLE_OPT) 110 | } 111 | // implies `#[interactive_clap(long)]` 112 | clap_attr_vec.push(quote! { long }); 113 | // type goes into output unchanged, otherwise it 114 | // prevents clap deriving correctly its `remove_many` thing 115 | cli_field = quote! { 116 | pub #ident_field: #ty 117 | }; 118 | } 119 | } 120 | _ => { 121 | abort_call_site!("Only option `TokenTree::Group` is needed") 122 | } 123 | } 124 | } 125 | } 126 | if attr.path.is_ident("doc") { 127 | doc_attr_vec.push(attr.into_token_stream()) 128 | } 129 | } 130 | if cli_field.is_empty() { 131 | return cli_field; 132 | }; 133 | let cfg_attrs = cfg_attr_vec.iter(); 134 | if !clap_attr_vec.is_empty() { 135 | let clap_attrs = clap_attr_vec.iter(); 136 | quote! { 137 | #(#cfg_attrs)* 138 | #(#doc_attr_vec)* 139 | #[clap(#(#clap_attrs, )*)] 140 | #cli_field 141 | } 142 | } else { 143 | quote! { 144 | #(#cfg_attrs)* 145 | #(#doc_attr_vec)* 146 | #cli_field 147 | } 148 | } 149 | }) 150 | .filter(|token_stream| !token_stream.is_empty()) 151 | .collect::>(); 152 | (fields, ident_skip_field_vec) 153 | } 154 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/derives/interactive_clap/structs/to_cli_trait/from_trait.rs: -------------------------------------------------------------------------------- 1 | use crate::LONG_VEC_MUTLIPLE_OPT; 2 | use proc_macro2::TokenStream; 3 | use proc_macro_error::abort_call_site; 4 | use quote::quote; 5 | 6 | /// returns the whole result `TokenStream` of derive logic of containing module 7 | pub fn token_stream( 8 | name: &syn::Ident, 9 | cli_name: &syn::Ident, 10 | input_fields: &syn::Fields, 11 | ident_skip_field_vec: &[syn::Ident], 12 | ) -> TokenStream { 13 | let fields_conversion = input_fields 14 | .iter() 15 | .map(|field| field_conversion(field, ident_skip_field_vec)) 16 | .filter(|token_stream| !token_stream.is_empty()); 17 | 18 | quote! { 19 | 20 | impl From<#name> for #cli_name { 21 | fn from(args: #name) -> Self { 22 | Self { 23 | #( #fields_conversion, )* 24 | } 25 | } 26 | } 27 | } 28 | } 29 | 30 | fn field_conversion(field: &syn::Field, ident_skip_field_vec: &[syn::Ident]) -> TokenStream { 31 | let ident_field = &field.clone().ident.expect("this field does not exist"); 32 | if ident_skip_field_vec.contains(ident_field) { 33 | quote!() 34 | } else { 35 | let ty = &field.ty; 36 | if field.attrs.iter().any(|attr| 37 | attr.path.is_ident("interactive_clap") && 38 | attr.tokens.clone().into_iter().any( 39 | |attr_token| 40 | matches!( 41 | attr_token, 42 | proc_macro2::TokenTree::Group(group) if group.stream().to_string() == LONG_VEC_MUTLIPLE_OPT 43 | ) 44 | ) 45 | ) { 46 | return quote! { 47 | #ident_field: args.#ident_field.into() 48 | }; 49 | } 50 | 51 | match &ty { 52 | syn::Type::Path(type_path) => match type_path.path.segments.first() { 53 | Some(path_segment) => { 54 | if path_segment.ident == "Option" || path_segment.ident == "bool" { 55 | quote! { 56 | #ident_field: args.#ident_field.into() 57 | } 58 | } else { 59 | quote! { 60 | #ident_field: Some(args.#ident_field.into()) 61 | } 62 | } 63 | } 64 | _ => abort_call_site!("Only option `PathSegment` is needed"), 65 | }, 66 | _ => abort_call_site!("Only option `Type::Path` is needed"), 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/derives/interactive_clap/structs/to_cli_trait/mod.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | `interactive_clap::ToCli` derive 3 | 4 | This module describes the derive logic of `#cli_name` struct used as `CliVariant` in 5 | implementation of `interactive_clap::ToCli`, which happens during derive of [`crate::InteractiveClap`] for `#name` struct. 6 | 7 | ```rust,ignore 8 | #[derive(Debug, Default, Clone, clap::Parser, interactive_clap::ToCliArgs)] 9 | #[clap(author, version, about, long_about = None)] 10 | pub struct #cli_name { 11 | #( #cli_fields, )* 12 | } 13 | 14 | impl interactive_clap::ToCli for #name { 15 | type CliVariant = #cli_name; 16 | } 17 | ``` 18 | 19 | Where `interactive_clap::ToCli` is: 20 | 21 | ```rust,ignore 22 | pub trait ToCli { 23 | type CliVariant; 24 | } 25 | ``` 26 | Additionally a [`clap::Parser`](https://docs.rs/clap/4.5.24/clap/trait.Parser.html) adapter 27 | for `#name` and `From<#name> for #cli_name` conversion are defined: 28 | 29 | ```rust,ignore 30 | impl #name { 31 | pub fn try_parse() -> Result<#cli_name, clap::Error> { 32 | <#cli_name as clap::Parser>::try_parse() 33 | } 34 | 35 | pub fn parse() -> #cli_name { 36 | <#cli_name as clap::Parser>::parse() 37 | } 38 | 39 | pub fn try_parse_from(itr: I) -> Result<#cli_name, clap::Error> 40 | where 41 | I: ::std::iter::IntoIterator, 42 | T: ::std::convert::Into<::std::ffi::OsString> + ::std::clone::Clone, 43 | { 44 | <#cli_name as clap::Parser>::try_parse_from(itr) 45 | } 46 | } 47 | 48 | impl From<#name> for #cli_name { 49 | fn from(args: #name) -> Self { 50 | Self { 51 | #( #fields_conversion, )* 52 | } 53 | } 54 | } 55 | ``` 56 | */ 57 | use proc_macro2::TokenStream; 58 | use quote::quote; 59 | 60 | /// returns the whole result `TokenStream` of derive logic of containing module 61 | pub fn token_stream( 62 | name: &syn::Ident, 63 | cli_name: &syn::Ident, 64 | input_fields: &syn::Fields, 65 | ) -> TokenStream { 66 | let (cli_variant_struct, ident_skip_field_vec) = 67 | cli_variant_struct::token_stream(name, cli_name, input_fields); 68 | 69 | let clap_parser_adapter = clap_parser_trait_adapter::token_stream(name, cli_name); 70 | let from_trait_impl = 71 | from_trait::token_stream(name, cli_name, input_fields, &ident_skip_field_vec); 72 | quote! { 73 | #cli_variant_struct 74 | 75 | impl interactive_clap::ToCli for #name { 76 | type CliVariant = #cli_name; 77 | } 78 | 79 | #clap_parser_adapter 80 | 81 | #from_trait_impl 82 | } 83 | } 84 | 85 | /// describes derive of `#cli_name` struct based on input `#name` struct 86 | pub(crate) mod cli_variant_struct; 87 | 88 | /// describes logic of derive of [`clap::Parser`](https://docs.rs/clap/4.5.24/clap/trait.Parser.html) adapter 89 | /// for `#name` struct, which returns instances of `#cli_name` struct 90 | mod clap_parser_trait_adapter; 91 | 92 | /// describes the derive of `impl From<#name> for #cli_name` 93 | mod from_trait; 94 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/derives/interactive_clap/structs/to_interactive_clap_context_scope_trait.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | `interactive_clap::ToInteractiveClapContextScope` derive 3 | 4 | This modules describes derive of `interactive_clap::ToInteractiveClapContextScope` trait for `#name` struct, 5 | which happens during derive of [`crate::InteractiveClap`] for `#name` struct: 6 | 7 | derive input `#name` 8 | 9 | ```rust,ignore 10 | struct #name { 11 | age: u64, 12 | first_name: String, 13 | } 14 | ``` 15 | 16 | gets transformed 17 | => 18 | 19 | ```rust,ignore 20 | impl #name pub struct InteractiveClapContextScopeFor#name { 21 | pub age: u64, 22 | pub first_name: String, 23 | } 24 | impl interactive_clap::ToInteractiveClapContextScope for #name { 25 | type InteractiveClapContextScope = InteractiveClapContextScopeFor#name; 26 | } 27 | ``` 28 | */ 29 | use proc_macro2::Span; 30 | use quote::quote; 31 | 32 | use super::common_field_methods as field_methods; 33 | 34 | /// returns the whole result `TokenStream` of derive logic of containing module 35 | pub fn token_stream(ast: &syn::DeriveInput, fields: &syn::Fields) -> proc_macro2::TokenStream { 36 | let name = &ast.ident; 37 | let context_scope_fields = fields 38 | .iter() 39 | .map(field_transform) 40 | .filter(|token_stream| !token_stream.is_empty()) 41 | .collect::>(); 42 | let interactive_clap_context_scope_for_struct = syn::Ident::new( 43 | &format!("InteractiveClapContextScopeFor{}", &name), 44 | Span::call_site(), 45 | ); 46 | quote! { 47 | pub struct #interactive_clap_context_scope_for_struct { 48 | #(#context_scope_fields,)* 49 | } 50 | impl interactive_clap::ToInteractiveClapContextScope for #name { 51 | type InteractiveClapContextScope = #interactive_clap_context_scope_for_struct; 52 | } 53 | } 54 | } 55 | 56 | fn field_transform(field: &syn::Field) -> proc_macro2::TokenStream { 57 | let ident_field = &field.ident.clone().expect("this field does not exist"); 58 | let ty = &field.ty; 59 | if !field_methods::with_subcommand::predicate(field) 60 | && !field_methods::with_subargs::predicate(field) 61 | { 62 | quote! { 63 | pub #ident_field: #ty 64 | } 65 | } else { 66 | quote!() 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/derives/mod.rs: -------------------------------------------------------------------------------- 1 | /// This module describes [`crate::InteractiveClap`] derive logic 2 | pub mod interactive_clap; 3 | pub mod to_cli_args; 4 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/derives/to_cli_args/methods/interactive_clap_attrs_cli_field.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | use proc_macro2::Span; 4 | use proc_macro_error::abort_call_site; 5 | use quote::{quote, ToTokens}; 6 | use syn; 7 | 8 | #[derive(Debug, Clone)] 9 | pub enum InteractiveClapAttrsCliField { 10 | RegularField(proc_macro2::TokenStream), 11 | SubcommandField(proc_macro2::TokenStream), 12 | } 13 | 14 | impl InteractiveClapAttrsCliField { 15 | pub fn new(field: syn::Field) -> Self { 16 | let ident_field = field.ident.clone().expect("this field does not exist"); 17 | let mut args_without_attrs = quote!(); 18 | let mut named_args = quote!(); 19 | let mut unnamed_args = quote!(); 20 | 21 | if !field.attrs.iter().any(|attr| attr.path.is_ident("clap")) { 22 | // BUGFIX: changed when this branch is being taken 23 | // from: field attributes are empty 24 | // to: there're no field attributes with `clap` identificator 25 | // 26 | // in order to allow `doc` attributes 27 | args_without_attrs = quote! { 28 | if let Some(arg) = &self.#ident_field { 29 | args.push_front(arg.to_string()) 30 | } 31 | }; 32 | } else { 33 | for attr in &field.attrs { 34 | if attr.path.is_ident("clap") { 35 | for attr_token in attr.tokens.clone() { 36 | match attr_token { 37 | proc_macro2::TokenTree::Group(group) => { 38 | for item in group.stream() { 39 | match &item { 40 | proc_macro2::TokenTree::Ident(ident) => { 41 | if ident == "subcommand" { 42 | return Self::SubcommandField(quote! { 43 | let mut args = self 44 | .#ident_field 45 | .as_ref() 46 | .map(|subcommand| subcommand.to_cli_args()) 47 | .unwrap_or_default(); 48 | }); 49 | } 50 | if ident == "flatten" { 51 | args_without_attrs = quote! { 52 | if let Some(arg) = &self.#ident_field { 53 | let mut to_cli_args = arg.to_cli_args(); 54 | to_cli_args.append(&mut args); 55 | std::mem::swap(&mut args, &mut to_cli_args); 56 | } 57 | }; 58 | } 59 | if ident == "value_enum" { 60 | args_without_attrs = quote! { 61 | if let Some(arg) = &self.#ident_field { 62 | args.push_front(arg.to_string()) 63 | } 64 | }; 65 | } else if ident == "long" { 66 | let ident_field_to_kebab_case_string = 67 | crate::helpers::to_kebab_case::to_kebab_case( 68 | ident_field.to_string(), 69 | ); 70 | let ident_field_to_kebab_case = &syn::LitStr::new( 71 | &ident_field_to_kebab_case_string, 72 | Span::call_site(), 73 | ); 74 | 75 | if field.ty.to_token_stream().to_string() == "bool" 76 | { 77 | unnamed_args = quote! { 78 | if self.#ident_field { 79 | args.push_front(std::concat!("--", #ident_field_to_kebab_case).to_string()); 80 | } 81 | }; 82 | } else { 83 | unnamed_args = quote! { 84 | if let Some(arg) = &self.#ident_field { 85 | args.push_front(arg.to_string()); 86 | args.push_front(std::concat!("--", #ident_field_to_kebab_case).to_string()); 87 | } 88 | }; 89 | if crate::helpers::type_starts_with_vec( 90 | &field.ty, 91 | ) { 92 | unnamed_args = quote! { 93 | for arg in self.#ident_field.iter().rev() { 94 | args.push_front(arg.to_string()); 95 | args.push_front(std::concat!("--", #ident_field_to_kebab_case).to_string()); 96 | } 97 | }; 98 | } 99 | } 100 | } 101 | } 102 | proc_macro2::TokenTree::Literal(literal) => { 103 | named_args = quote! { 104 | if let Some(arg) = &self.#ident_field { 105 | args.push_front(arg.to_string()); 106 | args.push_front(std::concat!("--", #literal).to_string()); 107 | } 108 | }; 109 | } 110 | _ => (), //abort_call_site!("Only option `TokenTree::Ident` is needed") 111 | }; 112 | } 113 | } 114 | _ => abort_call_site!("Only option `TokenTree::Group` is needed"), 115 | } 116 | } 117 | } 118 | } 119 | }; 120 | let token_stream_args: proc_macro2::TokenStream = if !named_args.is_empty() { 121 | named_args 122 | } else if !unnamed_args.is_empty() { 123 | unnamed_args 124 | } else if !args_without_attrs.is_empty() { 125 | args_without_attrs 126 | } else { 127 | quote!() 128 | }; 129 | Self::RegularField(token_stream_args) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/derives/to_cli_args/methods/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod interactive_clap_attrs_cli_field; 2 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/derives/to_cli_args/mod.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | use proc_macro2::{Span, TokenStream}; 4 | use proc_macro_error::abort_call_site; 5 | use quote::quote; 6 | use syn; 7 | 8 | use self::methods::interactive_clap_attrs_cli_field::InteractiveClapAttrsCliField; 9 | 10 | mod methods; 11 | 12 | pub fn impl_to_cli_args(ast: &syn::DeriveInput) -> TokenStream { 13 | let cli_name = &ast.ident; 14 | match &ast.data { 15 | syn::Data::Struct(data_struct) => { 16 | let mut args_subcommand = quote! { 17 | let mut args = std::collections::VecDeque::new(); 18 | }; 19 | let mut args_push_front_vec: Vec = Vec::new(); 20 | 21 | for field in data_struct.clone().fields.iter() { 22 | match InteractiveClapAttrsCliField::new(field.clone()) { 23 | InteractiveClapAttrsCliField::RegularField(regular_field_args) => { 24 | args_push_front_vec.push(regular_field_args) 25 | } 26 | InteractiveClapAttrsCliField::SubcommandField(subcommand_args) => { 27 | args_subcommand = subcommand_args 28 | } 29 | } 30 | } 31 | let args_push_front_vec = args_push_front_vec.into_iter().rev(); 32 | 33 | quote! { 34 | impl interactive_clap::ToCliArgs for #cli_name { 35 | fn to_cli_args(&self) -> std::collections::VecDeque { 36 | #args_subcommand; 37 | #(#args_push_front_vec; )* 38 | args 39 | } 40 | } 41 | } 42 | } 43 | syn::Data::Enum(syn::DataEnum { variants, .. }) => { 44 | let enum_variants = variants.iter().map(|variant| { 45 | let ident = &variant.ident; 46 | let variant_name_string = 47 | crate::helpers::to_kebab_case::to_kebab_case(ident.to_string()); 48 | let variant_name = &syn::LitStr::new(&variant_name_string, Span::call_site()); 49 | 50 | match &variant.fields { 51 | syn::Fields::Unnamed(_) => { 52 | quote! { 53 | Self::#ident(subcommand) => { 54 | let mut args = subcommand.to_cli_args(); 55 | args.push_front(#variant_name.to_owned()); 56 | args 57 | } 58 | } 59 | } 60 | syn::Fields::Unit => { 61 | quote! { 62 | Self::#ident => { 63 | let mut args = std::collections::VecDeque::new(); 64 | args.push_front(#variant_name.to_owned()); 65 | args 66 | } 67 | } 68 | } 69 | _ => abort_call_site!( 70 | "Only options `Fields::Unnamed` or `Fields::Unit` are needed" 71 | ), 72 | } 73 | }); 74 | quote! { 75 | impl interactive_clap::ToCliArgs for #cli_name { 76 | fn to_cli_args(&self) -> std::collections::VecDeque { 77 | match self { 78 | #( #enum_variants, )* 79 | } 80 | } 81 | } 82 | } 83 | } 84 | _ => abort_call_site!("`#[derive(InteractiveClap)]` only supports structs and enums"), 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/helpers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod snake_case_to_camel_case; 2 | pub mod to_kebab_case; 3 | 4 | pub fn type_starts_with_vec(ty: &syn::Type) -> bool { 5 | if let syn::Type::Path(type_path) = ty { 6 | if let Some(path_segment) = type_path.path.segments.first() { 7 | if path_segment.ident == "Vec" { 8 | return true; 9 | } 10 | } 11 | } 12 | false 13 | } 14 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/helpers/snake_case_to_camel_case.rs: -------------------------------------------------------------------------------- 1 | pub fn snake_case_to_camel_case(s: String) -> String { 2 | let s_vec: Vec = s 3 | .to_lowercase() 4 | .split('_') 5 | .map(|s| s.replacen(&s[..1], &s[..1].to_ascii_uppercase(), 1)) 6 | .collect(); 7 | s_vec.join("") 8 | } 9 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/helpers/to_kebab_case.rs: -------------------------------------------------------------------------------- 1 | pub fn to_kebab_case(s: String) -> String { 2 | let mut snake = String::new(); 3 | for (i, ch) in s.char_indices() { 4 | if i > 0 && ch.is_uppercase() { 5 | snake.push('-'); 6 | } 7 | snake.push(ch.to_ascii_lowercase()); 8 | } 9 | snake.as_str().replace('_', "-") 10 | } 11 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | use proc_macro::TokenStream; 4 | use proc_macro_error::proc_macro_error; 5 | 6 | #[macro_use] 7 | mod debug; 8 | 9 | mod derives; 10 | mod helpers; 11 | #[cfg(test)] 12 | mod tests; 13 | 14 | /// `#[interactive_clap(...)]` attribute used for specifying multiple values with `Vec<..>` type, 15 | /// by repeating corresponding flag `--field-name` (kebab case) for each value 16 | /// 17 | /// implies `#[interactive_clap(long)]` 18 | /// 19 | /// implies `#[interactive_clap(skip_interactive_input)]`, as it's not intended for interactive input 20 | pub(crate) const LONG_VEC_MUTLIPLE_OPT: &str = "long_vec_multiple_opt"; 21 | 22 | /// `#[interactive_clap(...)]` attribute which translates 1-to-1 into 23 | /// `#[clap(verbatim_doc_comment)]` 24 | /// More info on 25 | pub(crate) const VERBATIM_DOC_COMMENT: &str = "verbatim_doc_comment"; 26 | 27 | #[proc_macro_derive(InteractiveClap, attributes(interactive_clap))] 28 | #[proc_macro_error] 29 | pub fn interactive_clap(input: TokenStream) -> TokenStream { 30 | let ast = syn::parse_macro_input!(input); 31 | derives::interactive_clap::impl_interactive_clap(&ast).into() 32 | } 33 | 34 | #[proc_macro_derive(ToCliArgs, attributes(to_cli_args))] 35 | #[proc_macro_error] 36 | pub fn to_cli_args(input: TokenStream) -> TokenStream { 37 | let ast = syn::parse_macro_input!(input); 38 | derives::to_cli_args::impl_to_cli_args(&ast).into() 39 | } 40 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/tests/mod.rs: -------------------------------------------------------------------------------- 1 | mod test_simple_enum; 2 | mod test_simple_struct; 3 | 4 | fn pretty_codegen(ts: &proc_macro2::TokenStream) -> String { 5 | let file = syn::parse_file(&ts.to_string()).unwrap(); 6 | prettyplease::unparse(&file) 7 | } 8 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_enum__simple_enum-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: interactive-clap-derive/src/tests/test_simple_enum.rs 3 | expression: pretty_codegen(&to_cli_args_codegen) 4 | --- 5 | impl interactive_clap::ToCliArgs for CliMode { 6 | fn to_cli_args(&self) -> std::collections::VecDeque { 7 | match self { 8 | Self::Network => { 9 | let mut args = std::collections::VecDeque::new(); 10 | args.push_front("network".to_owned()); 11 | args 12 | } 13 | Self::Offline => { 14 | let mut args = std::collections::VecDeque::new(); 15 | args.push_front("offline".to_owned()); 16 | args 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_enum__simple_enum.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: interactive-clap-derive/src/tests/test_simple_enum.rs 3 | expression: pretty_codegen(&interactive_clap_codegen) 4 | --- 5 | #[derive(Debug, Clone, clap::Parser, interactive_clap::ToCliArgs)] 6 | pub enum CliMode { 7 | /// Prepare and, optionally, submit a new transaction with online mode 8 | Network, 9 | /// Prepare and, optionally, submit a new transaction with offline mode 10 | Offline, 11 | } 12 | impl interactive_clap::ToCli for Mode { 13 | type CliVariant = CliMode; 14 | } 15 | pub type InteractiveClapContextScopeForMode = ModeDiscriminants; 16 | impl interactive_clap::ToInteractiveClapContextScope for Mode { 17 | type InteractiveClapContextScope = InteractiveClapContextScopeForMode; 18 | } 19 | impl From for CliMode { 20 | fn from(command: Mode) -> Self { 21 | match command { 22 | Mode::Network => Self::Network, 23 | Mode::Offline => Self::Offline, 24 | } 25 | } 26 | } 27 | impl interactive_clap::FromCli for Mode { 28 | type FromCliContext = (); 29 | type FromCliError = color_eyre::eyre::Error; 30 | fn from_cli( 31 | mut optional_clap_variant: Option<::CliVariant>, 32 | context: Self::FromCliContext, 33 | ) -> interactive_clap::ResultFromCli< 34 | ::CliVariant, 35 | Self::FromCliError, 36 | > 37 | where 38 | Self: Sized + interactive_clap::ToCli, 39 | { 40 | loop { 41 | return match optional_clap_variant { 42 | Some(CliMode::Network) => { 43 | interactive_clap::ResultFromCli::Ok(CliMode::Network) 44 | } 45 | Some(CliMode::Offline) => { 46 | interactive_clap::ResultFromCli::Ok(CliMode::Offline) 47 | } 48 | None => { 49 | match Self::choose_variant(context.clone()) { 50 | interactive_clap::ResultFromCli::Ok(cli_args) => { 51 | optional_clap_variant = Some(cli_args); 52 | continue; 53 | } 54 | result => return result, 55 | } 56 | } 57 | }; 58 | } 59 | } 60 | } 61 | impl Mode { 62 | pub fn choose_variant( 63 | context: (), 64 | ) -> interactive_clap::ResultFromCli< 65 | ::CliVariant, 66 | ::FromCliError, 67 | > {} 68 | pub fn try_parse() -> Result { 69 | ::try_parse() 70 | } 71 | pub fn parse() -> CliMode { 72 | ::parse() 73 | } 74 | pub fn try_parse_from(itr: I) -> Result 75 | where 76 | I: ::std::iter::IntoIterator, 77 | T: ::std::convert::Into<::std::ffi::OsString> + ::std::clone::Clone, 78 | { 79 | ::try_parse_from(itr) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_enum__simple_enum_with_strum_discriminants-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: interactive-clap-derive/src/tests/test_simple_enum.rs 3 | expression: pretty_codegen(&to_cli_args_codegen) 4 | --- 5 | impl interactive_clap::ToCliArgs for CliMode { 6 | fn to_cli_args(&self) -> std::collections::VecDeque { 7 | match self { 8 | Self::Network => { 9 | let mut args = std::collections::VecDeque::new(); 10 | args.push_front("network".to_owned()); 11 | args 12 | } 13 | Self::Offline => { 14 | let mut args = std::collections::VecDeque::new(); 15 | args.push_front("offline".to_owned()); 16 | args 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_enum__simple_enum_with_strum_discriminants.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: interactive-clap-derive/src/tests/test_simple_enum.rs 3 | expression: pretty_codegen(&interactive_clap_codegen) 4 | --- 5 | #[derive(Debug, Clone, clap::Parser, interactive_clap::ToCliArgs)] 6 | pub enum CliMode { 7 | /// Prepare and, optionally, submit a new transaction with online mode 8 | Network, 9 | /// Prepare and, optionally, submit a new transaction with offline mode 10 | Offline, 11 | } 12 | impl interactive_clap::ToCli for Mode { 13 | type CliVariant = CliMode; 14 | } 15 | pub type InteractiveClapContextScopeForMode = ModeDiscriminants; 16 | impl interactive_clap::ToInteractiveClapContextScope for Mode { 17 | type InteractiveClapContextScope = InteractiveClapContextScopeForMode; 18 | } 19 | impl From for CliMode { 20 | fn from(command: Mode) -> Self { 21 | match command { 22 | Mode::Network => Self::Network, 23 | Mode::Offline => Self::Offline, 24 | } 25 | } 26 | } 27 | impl interactive_clap::FromCli for Mode { 28 | type FromCliContext = (); 29 | type FromCliError = color_eyre::eyre::Error; 30 | fn from_cli( 31 | mut optional_clap_variant: Option<::CliVariant>, 32 | context: Self::FromCliContext, 33 | ) -> interactive_clap::ResultFromCli< 34 | ::CliVariant, 35 | Self::FromCliError, 36 | > 37 | where 38 | Self: Sized + interactive_clap::ToCli, 39 | { 40 | loop { 41 | return match optional_clap_variant { 42 | Some(CliMode::Network) => { 43 | interactive_clap::ResultFromCli::Ok(CliMode::Network) 44 | } 45 | Some(CliMode::Offline) => { 46 | interactive_clap::ResultFromCli::Ok(CliMode::Offline) 47 | } 48 | None => { 49 | match Self::choose_variant(context.clone()) { 50 | interactive_clap::ResultFromCli::Ok(cli_args) => { 51 | optional_clap_variant = Some(cli_args); 52 | continue; 53 | } 54 | result => return result, 55 | } 56 | } 57 | }; 58 | } 59 | } 60 | } 61 | impl Mode { 62 | pub fn choose_variant( 63 | context: (), 64 | ) -> interactive_clap::ResultFromCli< 65 | ::CliVariant, 66 | ::FromCliError, 67 | > { 68 | use interactive_clap::SelectVariantOrBack; 69 | use inquire::Select; 70 | use strum::{EnumMessage, IntoEnumIterator}; 71 | let selected_variant = Select::new( 72 | concat!(r" A little beautiful comment about our choice",).trim(), 73 | ModeDiscriminants::iter() 74 | .map(SelectVariantOrBack::Variant) 75 | .chain([SelectVariantOrBack::Back]) 76 | .collect(), 77 | ) 78 | .prompt(); 79 | match selected_variant { 80 | Ok(SelectVariantOrBack::Variant(variant)) => { 81 | let cli_args = match variant { 82 | ModeDiscriminants::Network => CliMode::Network, 83 | ModeDiscriminants::Offline => CliMode::Offline, 84 | }; 85 | return interactive_clap::ResultFromCli::Ok(cli_args); 86 | } 87 | Ok(SelectVariantOrBack::Back) => return interactive_clap::ResultFromCli::Back, 88 | Err( 89 | inquire::error::InquireError::OperationCanceled 90 | | inquire::error::InquireError::OperationInterrupted, 91 | ) => return interactive_clap::ResultFromCli::Cancel(None), 92 | Err(err) => return interactive_clap::ResultFromCli::Err(None, err.into()), 93 | } 94 | } 95 | pub fn try_parse() -> Result { 96 | ::try_parse() 97 | } 98 | pub fn parse() -> CliMode { 99 | ::parse() 100 | } 101 | pub fn try_parse_from(itr: I) -> Result 102 | where 103 | I: ::std::iter::IntoIterator, 104 | T: ::std::convert::Into<::std::ffi::OsString> + ::std::clone::Clone, 105 | { 106 | ::try_parse_from(itr) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__bug_fix_of_to_cli_args_derive-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: interactive-clap-derive/src/tests/test_simple_struct.rs 3 | expression: pretty_codegen(&to_cli_args_codegen) 4 | --- 5 | impl interactive_clap::ToCliArgs for CliViewAccountSummary { 6 | fn to_cli_args(&self) -> std::collections::VecDeque { 7 | let mut args = std::collections::VecDeque::new(); 8 | if let Some(arg) = &self.account_id { 9 | args.push_front(arg.to_string()) 10 | } 11 | args 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__bug_fix_of_to_cli_args_derive.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: interactive-clap-derive/src/tests/test_simple_struct.rs 3 | expression: pretty_codegen(&interactive_clap_codegen) 4 | --- 5 | #[derive(Debug, Default, Clone, clap::Parser, interactive_clap::ToCliArgs)] 6 | #[clap(author, version, about, long_about = None)] 7 | pub struct CliViewAccountSummary { 8 | /// What Account ID do you need to view? 9 | pub account_id: Option< 10 | ::CliVariant, 11 | >, 12 | } 13 | impl interactive_clap::ToCli for ViewAccountSummary { 14 | type CliVariant = CliViewAccountSummary; 15 | } 16 | impl ViewAccountSummary { 17 | pub fn try_parse() -> Result { 18 | ::try_parse() 19 | } 20 | pub fn parse() -> CliViewAccountSummary { 21 | ::parse() 22 | } 23 | pub fn try_parse_from(itr: I) -> Result 24 | where 25 | I: ::std::iter::IntoIterator, 26 | T: ::std::convert::Into<::std::ffi::OsString> + ::std::clone::Clone, 27 | { 28 | ::try_parse_from(itr) 29 | } 30 | } 31 | impl From for CliViewAccountSummary { 32 | fn from(args: ViewAccountSummary) -> Self { 33 | Self { 34 | account_id: Some(args.account_id.into()), 35 | } 36 | } 37 | } 38 | impl ViewAccountSummary { 39 | fn input_account_id( 40 | _context: &(), 41 | ) -> color_eyre::eyre::Result> { 42 | match inquire::CustomType::new( 43 | concat!(r" What Account ID do you need to view?",).trim(), 44 | ) 45 | .prompt() 46 | { 47 | Ok(value) => Ok(Some(value)), 48 | Err( 49 | inquire::error::InquireError::OperationCanceled 50 | | inquire::error::InquireError::OperationInterrupted, 51 | ) => Ok(None), 52 | Err(err) => Err(err.into()), 53 | } 54 | } 55 | } 56 | pub struct InteractiveClapContextScopeForViewAccountSummary { 57 | pub account_id: crate::types::account_id::AccountId, 58 | } 59 | impl interactive_clap::ToInteractiveClapContextScope for ViewAccountSummary { 60 | type InteractiveClapContextScope = InteractiveClapContextScopeForViewAccountSummary; 61 | } 62 | impl interactive_clap::FromCli for ViewAccountSummary { 63 | type FromCliContext = (); 64 | type FromCliError = color_eyre::eyre::Error; 65 | fn from_cli( 66 | optional_clap_variant: Option<::CliVariant>, 67 | context: Self::FromCliContext, 68 | ) -> interactive_clap::ResultFromCli< 69 | ::CliVariant, 70 | Self::FromCliError, 71 | > 72 | where 73 | Self: Sized + interactive_clap::ToCli, 74 | { 75 | let mut clap_variant = optional_clap_variant.clone().unwrap_or_default(); 76 | if clap_variant.account_id.is_none() { 77 | clap_variant 78 | .account_id = match Self::input_account_id(&context) { 79 | Ok(Some(account_id)) => Some(account_id), 80 | Ok(None) => { 81 | return interactive_clap::ResultFromCli::Cancel(Some(clap_variant)); 82 | } 83 | Err(err) => { 84 | return interactive_clap::ResultFromCli::Err(Some(clap_variant), err); 85 | } 86 | }; 87 | } 88 | let account_id = clap_variant.account_id.clone().expect("Unexpected error"); 89 | let new_context_scope = InteractiveClapContextScopeForViewAccountSummary { 90 | account_id: account_id.into(), 91 | }; 92 | interactive_clap::ResultFromCli::Ok(clap_variant) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__doc_comments_propagate-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: interactive-clap-derive/src/tests/test_simple_struct.rs 3 | expression: pretty_codegen(&to_cli_args_codegen) 4 | --- 5 | impl interactive_clap::ToCliArgs for CliArgs { 6 | fn to_cli_args(&self) -> std::collections::VecDeque { 7 | let mut args = std::collections::VecDeque::new(); 8 | if self.third_field { 9 | args.push_front(std::concat!("--", "third-field").to_string()); 10 | } 11 | if let Some(arg) = &self.second_field { 12 | args.push_front(arg.to_string()); 13 | args.push_front(std::concat!("--", "second-field").to_string()); 14 | } 15 | if let Some(arg) = &self.first_field { 16 | args.push_front(arg.to_string()); 17 | args.push_front(std::concat!("--", "first-field").to_string()); 18 | } 19 | args 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__doc_comments_propagate.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: interactive-clap-derive/src/tests/test_simple_struct.rs 3 | expression: pretty_codegen(&interactive_clap_codegen) 4 | --- 5 | #[derive(Debug, Default, Clone, clap::Parser, interactive_clap::ToCliArgs)] 6 | #[clap(author, version, about, long_about = None)] 7 | pub struct CliArgs { 8 | /// short first field description 9 | /// 10 | /// a longer paragraph, describing the usage and stuff with first field's 11 | /// awarenes of its possible applications 12 | #[clap(long)] 13 | pub first_field: Option<::CliVariant>, 14 | /// short second field description 15 | /// 16 | /// a longer paragraph, describing the usage and stuff with second field's 17 | /// awareness of its possible applications 18 | #[clap(long, verbatim_doc_comment)] 19 | pub second_field: Option<::CliVariant>, 20 | /// short third field description 21 | /// 22 | /// a longer paragraph, describing the usage and stuff with third field's 23 | /// awareness of its possible applications 24 | #[clap(long, verbatim_doc_comment)] 25 | pub third_field: bool, 26 | } 27 | impl interactive_clap::ToCli for Args { 28 | type CliVariant = CliArgs; 29 | } 30 | impl Args { 31 | pub fn try_parse() -> Result { 32 | ::try_parse() 33 | } 34 | pub fn parse() -> CliArgs { 35 | ::parse() 36 | } 37 | pub fn try_parse_from(itr: I) -> Result 38 | where 39 | I: ::std::iter::IntoIterator, 40 | T: ::std::convert::Into<::std::ffi::OsString> + ::std::clone::Clone, 41 | { 42 | ::try_parse_from(itr) 43 | } 44 | } 45 | impl From for CliArgs { 46 | fn from(args: Args) -> Self { 47 | Self { 48 | first_field: Some(args.first_field.into()), 49 | second_field: Some(args.second_field.into()), 50 | third_field: args.third_field.into(), 51 | } 52 | } 53 | } 54 | impl Args {} 55 | pub struct InteractiveClapContextScopeForArgs { 56 | pub first_field: u64, 57 | pub second_field: String, 58 | pub third_field: bool, 59 | } 60 | impl interactive_clap::ToInteractiveClapContextScope for Args { 61 | type InteractiveClapContextScope = InteractiveClapContextScopeForArgs; 62 | } 63 | impl interactive_clap::FromCli for Args { 64 | type FromCliContext = (); 65 | type FromCliError = color_eyre::eyre::Error; 66 | fn from_cli( 67 | optional_clap_variant: Option<::CliVariant>, 68 | context: Self::FromCliContext, 69 | ) -> interactive_clap::ResultFromCli< 70 | ::CliVariant, 71 | Self::FromCliError, 72 | > 73 | where 74 | Self: Sized + interactive_clap::ToCli, 75 | { 76 | let mut clap_variant = optional_clap_variant.clone().unwrap_or_default(); 77 | let first_field = clap_variant.first_field.clone(); 78 | let second_field = clap_variant.second_field.clone(); 79 | let third_field = clap_variant.third_field.clone(); 80 | let new_context_scope = InteractiveClapContextScopeForArgs { 81 | first_field: first_field.into(), 82 | second_field: second_field.into(), 83 | third_field: third_field.into(), 84 | }; 85 | interactive_clap::ResultFromCli::Ok(clap_variant) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__flag-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: interactive-clap-derive/src/tests/test_simple_struct.rs 3 | expression: pretty_codegen(&to_cli_args_codegen) 4 | --- 5 | impl interactive_clap::ToCliArgs for CliArgs { 6 | fn to_cli_args(&self) -> std::collections::VecDeque { 7 | let mut args = std::collections::VecDeque::new(); 8 | if self.offline { 9 | args.push_front(std::concat!("--", "offline").to_string()); 10 | } 11 | args 12 | } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__flag.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: interactive-clap-derive/src/tests/test_simple_struct.rs 3 | expression: pretty_codegen(&interactive_clap_codegen) 4 | --- 5 | #[derive(Debug, Default, Clone, clap::Parser, interactive_clap::ToCliArgs)] 6 | #[clap(author, version, about, long_about = None)] 7 | pub struct CliArgs { 8 | /// Offline mode 9 | #[clap(long)] 10 | pub offline: bool, 11 | } 12 | impl interactive_clap::ToCli for Args { 13 | type CliVariant = CliArgs; 14 | } 15 | impl Args { 16 | pub fn try_parse() -> Result { 17 | ::try_parse() 18 | } 19 | pub fn parse() -> CliArgs { 20 | ::parse() 21 | } 22 | pub fn try_parse_from(itr: I) -> Result 23 | where 24 | I: ::std::iter::IntoIterator, 25 | T: ::std::convert::Into<::std::ffi::OsString> + ::std::clone::Clone, 26 | { 27 | ::try_parse_from(itr) 28 | } 29 | } 30 | impl From for CliArgs { 31 | fn from(args: Args) -> Self { 32 | Self { 33 | offline: args.offline.into(), 34 | } 35 | } 36 | } 37 | impl Args { 38 | fn input_offline(_context: &()) -> color_eyre::eyre::Result> { 39 | match inquire::CustomType::new(concat!(r" Offline mode",).trim()).prompt() { 40 | Ok(value) => Ok(Some(value)), 41 | Err( 42 | inquire::error::InquireError::OperationCanceled 43 | | inquire::error::InquireError::OperationInterrupted, 44 | ) => Ok(None), 45 | Err(err) => Err(err.into()), 46 | } 47 | } 48 | } 49 | pub struct InteractiveClapContextScopeForArgs { 50 | pub offline: bool, 51 | } 52 | impl interactive_clap::ToInteractiveClapContextScope for Args { 53 | type InteractiveClapContextScope = InteractiveClapContextScopeForArgs; 54 | } 55 | impl interactive_clap::FromCli for Args { 56 | type FromCliContext = (); 57 | type FromCliError = color_eyre::eyre::Error; 58 | fn from_cli( 59 | optional_clap_variant: Option<::CliVariant>, 60 | context: Self::FromCliContext, 61 | ) -> interactive_clap::ResultFromCli< 62 | ::CliVariant, 63 | Self::FromCliError, 64 | > 65 | where 66 | Self: Sized + interactive_clap::ToCli, 67 | { 68 | let mut clap_variant = optional_clap_variant.clone().unwrap_or_default(); 69 | let offline = clap_variant.offline.clone(); 70 | let new_context_scope = InteractiveClapContextScopeForArgs { 71 | offline: offline.into(), 72 | }; 73 | interactive_clap::ResultFromCli::Ok(clap_variant) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__simple_struct-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: interactive-clap-derive/src/tests/test_simple_struct.rs 3 | expression: pretty_codegen(&to_cli_args_codegen) 4 | --- 5 | impl interactive_clap::ToCliArgs for CliArgs { 6 | fn to_cli_args(&self) -> std::collections::VecDeque { 7 | let mut args = std::collections::VecDeque::new(); 8 | if let Some(arg) = &self.second_name { 9 | args.push_front(arg.to_string()) 10 | } 11 | if let Some(arg) = &self.first_name { 12 | args.push_front(arg.to_string()) 13 | } 14 | if let Some(arg) = &self.age { 15 | args.push_front(arg.to_string()) 16 | } 17 | args 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__simple_struct.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: interactive-clap-derive/src/tests/test_simple_struct.rs 3 | expression: pretty_codegen(&interactive_clap_codegen) 4 | --- 5 | #[derive(Debug, Default, Clone, clap::Parser, interactive_clap::ToCliArgs)] 6 | #[clap(author, version, about, long_about = None)] 7 | pub struct CliArgs { 8 | pub age: Option<::CliVariant>, 9 | pub first_name: Option<::CliVariant>, 10 | pub second_name: Option<::CliVariant>, 11 | } 12 | impl interactive_clap::ToCli for Args { 13 | type CliVariant = CliArgs; 14 | } 15 | impl Args { 16 | pub fn try_parse() -> Result { 17 | ::try_parse() 18 | } 19 | pub fn parse() -> CliArgs { 20 | ::parse() 21 | } 22 | pub fn try_parse_from(itr: I) -> Result 23 | where 24 | I: ::std::iter::IntoIterator, 25 | T: ::std::convert::Into<::std::ffi::OsString> + ::std::clone::Clone, 26 | { 27 | ::try_parse_from(itr) 28 | } 29 | } 30 | impl From for CliArgs { 31 | fn from(args: Args) -> Self { 32 | Self { 33 | age: Some(args.age.into()), 34 | first_name: Some(args.first_name.into()), 35 | second_name: Some(args.second_name.into()), 36 | } 37 | } 38 | } 39 | impl Args { 40 | fn input_age(_context: &()) -> color_eyre::eyre::Result> { 41 | match inquire::CustomType::new("age").prompt() { 42 | Ok(value) => Ok(Some(value)), 43 | Err( 44 | inquire::error::InquireError::OperationCanceled 45 | | inquire::error::InquireError::OperationInterrupted, 46 | ) => Ok(None), 47 | Err(err) => Err(err.into()), 48 | } 49 | } 50 | fn input_first_name(_context: &()) -> color_eyre::eyre::Result> { 51 | match inquire::CustomType::new("first_name").prompt() { 52 | Ok(value) => Ok(Some(value)), 53 | Err( 54 | inquire::error::InquireError::OperationCanceled 55 | | inquire::error::InquireError::OperationInterrupted, 56 | ) => Ok(None), 57 | Err(err) => Err(err.into()), 58 | } 59 | } 60 | fn input_second_name(_context: &()) -> color_eyre::eyre::Result> { 61 | match inquire::CustomType::new("second_name").prompt() { 62 | Ok(value) => Ok(Some(value)), 63 | Err( 64 | inquire::error::InquireError::OperationCanceled 65 | | inquire::error::InquireError::OperationInterrupted, 66 | ) => Ok(None), 67 | Err(err) => Err(err.into()), 68 | } 69 | } 70 | } 71 | pub struct InteractiveClapContextScopeForArgs { 72 | pub age: u64, 73 | pub first_name: String, 74 | pub second_name: String, 75 | } 76 | impl interactive_clap::ToInteractiveClapContextScope for Args { 77 | type InteractiveClapContextScope = InteractiveClapContextScopeForArgs; 78 | } 79 | impl interactive_clap::FromCli for Args { 80 | type FromCliContext = (); 81 | type FromCliError = color_eyre::eyre::Error; 82 | fn from_cli( 83 | optional_clap_variant: Option<::CliVariant>, 84 | context: Self::FromCliContext, 85 | ) -> interactive_clap::ResultFromCli< 86 | ::CliVariant, 87 | Self::FromCliError, 88 | > 89 | where 90 | Self: Sized + interactive_clap::ToCli, 91 | { 92 | let mut clap_variant = optional_clap_variant.clone().unwrap_or_default(); 93 | if clap_variant.age.is_none() { 94 | clap_variant 95 | .age = match Self::input_age(&context) { 96 | Ok(Some(age)) => Some(age), 97 | Ok(None) => { 98 | return interactive_clap::ResultFromCli::Cancel(Some(clap_variant)); 99 | } 100 | Err(err) => { 101 | return interactive_clap::ResultFromCli::Err(Some(clap_variant), err); 102 | } 103 | }; 104 | } 105 | let age = clap_variant.age.clone().expect("Unexpected error"); 106 | if clap_variant.first_name.is_none() { 107 | clap_variant 108 | .first_name = match Self::input_first_name(&context) { 109 | Ok(Some(first_name)) => Some(first_name), 110 | Ok(None) => { 111 | return interactive_clap::ResultFromCli::Cancel(Some(clap_variant)); 112 | } 113 | Err(err) => { 114 | return interactive_clap::ResultFromCli::Err(Some(clap_variant), err); 115 | } 116 | }; 117 | } 118 | let first_name = clap_variant.first_name.clone().expect("Unexpected error"); 119 | if clap_variant.second_name.is_none() { 120 | clap_variant 121 | .second_name = match Self::input_second_name(&context) { 122 | Ok(Some(second_name)) => Some(second_name), 123 | Ok(None) => { 124 | return interactive_clap::ResultFromCli::Cancel(Some(clap_variant)); 125 | } 126 | Err(err) => { 127 | return interactive_clap::ResultFromCli::Err(Some(clap_variant), err); 128 | } 129 | }; 130 | } 131 | let second_name = clap_variant.second_name.clone().expect("Unexpected error"); 132 | let new_context_scope = InteractiveClapContextScopeForArgs { 133 | age: age.into(), 134 | first_name: first_name.into(), 135 | second_name: second_name.into(), 136 | }; 137 | interactive_clap::ResultFromCli::Ok(clap_variant) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__simple_struct_with_named_arg-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: interactive-clap-derive/src/tests/test_simple_struct.rs 3 | expression: pretty_codegen(&to_cli_args_codegen) 4 | --- 5 | impl interactive_clap::ToCliArgs for CliAccount { 6 | fn to_cli_args(&self) -> std::collections::VecDeque { 7 | let mut args = self 8 | .field_name 9 | .as_ref() 10 | .map(|subcommand| subcommand.to_cli_args()) 11 | .unwrap_or_default(); 12 | args 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__simple_struct_with_named_arg.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: interactive-clap-derive/src/tests/test_simple_struct.rs 3 | expression: pretty_codegen(&interactive_clap_codegen) 4 | --- 5 | #[derive(Debug, Default, Clone, clap::Parser, interactive_clap::ToCliArgs)] 6 | #[clap(author, version, about, long_about = None)] 7 | pub struct CliAccount { 8 | #[clap(subcommand)] 9 | pub field_name: Option, 10 | } 11 | impl interactive_clap::ToCli for Account { 12 | type CliVariant = CliAccount; 13 | } 14 | impl Account { 15 | pub fn try_parse() -> Result { 16 | ::try_parse() 17 | } 18 | pub fn parse() -> CliAccount { 19 | ::parse() 20 | } 21 | pub fn try_parse_from(itr: I) -> Result 22 | where 23 | I: ::std::iter::IntoIterator, 24 | T: ::std::convert::Into<::std::ffi::OsString> + ::std::clone::Clone, 25 | { 26 | ::try_parse_from(itr) 27 | } 28 | } 29 | impl From for CliAccount { 30 | fn from(args: Account) -> Self { 31 | Self { 32 | field_name: Some(args.field_name.into()), 33 | } 34 | } 35 | } 36 | impl Account {} 37 | pub struct InteractiveClapContextScopeForAccount {} 38 | impl interactive_clap::ToInteractiveClapContextScope for Account { 39 | type InteractiveClapContextScope = InteractiveClapContextScopeForAccount; 40 | } 41 | impl interactive_clap::FromCli for Account { 42 | type FromCliContext = (); 43 | type FromCliError = color_eyre::eyre::Error; 44 | fn from_cli( 45 | optional_clap_variant: Option<::CliVariant>, 46 | context: Self::FromCliContext, 47 | ) -> interactive_clap::ResultFromCli< 48 | ::CliVariant, 49 | Self::FromCliError, 50 | > 51 | where 52 | Self: Sized + interactive_clap::ToCli, 53 | { 54 | let mut clap_variant = optional_clap_variant.clone().unwrap_or_default(); 55 | let new_context_scope = InteractiveClapContextScopeForAccount { 56 | }; 57 | let optional_field = match clap_variant.field_name.take() { 58 | Some(ClapNamedArgSenderForAccount::FieldName(cli_arg)) => Some(cli_arg), 59 | None => None, 60 | }; 61 | match ::from_cli( 62 | optional_field, 63 | context.into(), 64 | ) { 65 | interactive_clap::ResultFromCli::Ok(cli_field) => { 66 | clap_variant 67 | .field_name = Some( 68 | ClapNamedArgSenderForAccount::FieldName(cli_field), 69 | ); 70 | } 71 | interactive_clap::ResultFromCli::Cancel(optional_cli_field) => { 72 | clap_variant 73 | .field_name = optional_cli_field 74 | .map(ClapNamedArgSenderForAccount::FieldName); 75 | return interactive_clap::ResultFromCli::Cancel(Some(clap_variant)); 76 | } 77 | interactive_clap::ResultFromCli::Back => { 78 | return interactive_clap::ResultFromCli::Back; 79 | } 80 | interactive_clap::ResultFromCli::Err(optional_cli_field, err) => { 81 | clap_variant 82 | .field_name = optional_cli_field 83 | .map(ClapNamedArgSenderForAccount::FieldName); 84 | return interactive_clap::ResultFromCli::Err(Some(clap_variant), err); 85 | } 86 | }; 87 | interactive_clap::ResultFromCli::Ok(clap_variant) 88 | } 89 | } 90 | #[derive(Debug, Clone, clap::Parser, interactive_clap_derive::ToCliArgs)] 91 | pub enum ClapNamedArgSenderForAccount { 92 | FieldName(::CliVariant), 93 | } 94 | impl From for ClapNamedArgSenderForAccount { 95 | fn from(item: Sender) -> Self { 96 | Self::FieldName(::CliVariant::from(item)) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__vec_multiple_opt-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: interactive-clap-derive/src/tests/test_simple_struct.rs 3 | expression: pretty_codegen(&to_cli_args_codegen) 4 | --- 5 | impl interactive_clap::ToCliArgs for CliArgs { 6 | fn to_cli_args(&self) -> std::collections::VecDeque { 7 | let mut args = std::collections::VecDeque::new(); 8 | for arg in self.env.iter().rev() { 9 | args.push_front(arg.to_string()); 10 | args.push_front(std::concat!("--", "env").to_string()); 11 | } 12 | args 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__vec_multiple_opt.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: interactive-clap-derive/src/tests/test_simple_struct.rs 3 | expression: pretty_codegen(&interactive_clap_codegen) 4 | --- 5 | #[derive(Debug, Default, Clone, clap::Parser, interactive_clap::ToCliArgs)] 6 | #[clap(author, version, about, long_about = None)] 7 | pub struct CliArgs { 8 | #[clap(long)] 9 | pub env: Vec, 10 | } 11 | impl interactive_clap::ToCli for Args { 12 | type CliVariant = CliArgs; 13 | } 14 | impl Args { 15 | pub fn try_parse() -> Result { 16 | ::try_parse() 17 | } 18 | pub fn parse() -> CliArgs { 19 | ::parse() 20 | } 21 | pub fn try_parse_from(itr: I) -> Result 22 | where 23 | I: ::std::iter::IntoIterator, 24 | T: ::std::convert::Into<::std::ffi::OsString> + ::std::clone::Clone, 25 | { 26 | ::try_parse_from(itr) 27 | } 28 | } 29 | impl From for CliArgs { 30 | fn from(args: Args) -> Self { 31 | Self { env: args.env.into() } 32 | } 33 | } 34 | impl Args {} 35 | pub struct InteractiveClapContextScopeForArgs { 36 | pub env: Vec, 37 | } 38 | impl interactive_clap::ToInteractiveClapContextScope for Args { 39 | type InteractiveClapContextScope = InteractiveClapContextScopeForArgs; 40 | } 41 | impl interactive_clap::FromCli for Args { 42 | type FromCliContext = (); 43 | type FromCliError = color_eyre::eyre::Error; 44 | fn from_cli( 45 | optional_clap_variant: Option<::CliVariant>, 46 | context: Self::FromCliContext, 47 | ) -> interactive_clap::ResultFromCli< 48 | ::CliVariant, 49 | Self::FromCliError, 50 | > 51 | where 52 | Self: Sized + interactive_clap::ToCli, 53 | { 54 | let mut clap_variant = optional_clap_variant.clone().unwrap_or_default(); 55 | let env = clap_variant.env.clone(); 56 | let new_context_scope = InteractiveClapContextScopeForArgs { 57 | env: env.into(), 58 | }; 59 | interactive_clap::ResultFromCli::Ok(clap_variant) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/tests/test_simple_enum.rs: -------------------------------------------------------------------------------- 1 | use super::pretty_codegen; 2 | 3 | #[test] 4 | fn test_simple_enum() { 5 | let input = syn::parse_quote! { 6 | pub enum Mode { 7 | /// Prepare and, optionally, submit a new transaction with online mode 8 | Network, 9 | /// Prepare and, optionally, submit a new transaction with offline mode 10 | Offline, 11 | } 12 | }; 13 | 14 | let interactive_clap_codegen = crate::derives::interactive_clap::impl_interactive_clap(&input); 15 | insta::assert_snapshot!(pretty_codegen(&interactive_clap_codegen)); 16 | 17 | let step_one_output = syn::parse_quote! { 18 | pub enum CliMode { 19 | /// Prepare and, optionally, submit a new transaction with online mode 20 | Network, 21 | /// Prepare and, optionally, submit a new transaction with offline mode 22 | Offline, 23 | } 24 | }; 25 | 26 | let to_cli_args_codegen = crate::derives::to_cli_args::impl_to_cli_args(&step_one_output); 27 | insta::assert_snapshot!(pretty_codegen(&to_cli_args_codegen)); 28 | } 29 | 30 | #[test] 31 | fn test_simple_enum_with_strum_discriminants() { 32 | let input = syn::parse_quote! { 33 | #[strum_discriminants(derive(EnumMessage, EnumIter))] 34 | /// A little beautiful comment about our choice 35 | pub enum Mode { 36 | /// Prepare and, optionally, submit a new transaction with online mode 37 | #[strum_discriminants(strum(message = "Yes, I keep it simple"))] 38 | Network, 39 | /// Prepare and, optionally, submit a new transaction with offline mode 40 | #[strum_discriminants(strum( 41 | message = "No, I want to work in no-network (air-gapped) environment" 42 | ))] 43 | Offline, 44 | } 45 | }; 46 | 47 | let interactive_clap_codegen = crate::derives::interactive_clap::impl_interactive_clap(&input); 48 | insta::assert_snapshot!(pretty_codegen(&interactive_clap_codegen)); 49 | 50 | let step_one_output = syn::parse_quote! { 51 | pub enum CliMode { 52 | /// Prepare and, optionally, submit a new transaction with online mode 53 | Network, 54 | /// Prepare and, optionally, submit a new transaction with offline mode 55 | Offline, 56 | } 57 | }; 58 | 59 | let to_cli_args_codegen = crate::derives::to_cli_args::impl_to_cli_args(&step_one_output); 60 | insta::assert_snapshot!(pretty_codegen(&to_cli_args_codegen)); 61 | } 62 | -------------------------------------------------------------------------------- /interactive-clap-derive/src/tests/test_simple_struct.rs: -------------------------------------------------------------------------------- 1 | use super::pretty_codegen; 2 | use crate::derives::interactive_clap::to_cli_args_structs_test_bridge; 3 | 4 | #[test] 5 | fn test_simple_struct() { 6 | let input = syn::parse_quote! { 7 | struct Args { 8 | age: u64, 9 | first_name: String, 10 | second_name: String, 11 | } 12 | }; 13 | 14 | let interactive_clap_codegen = crate::derives::interactive_clap::impl_interactive_clap(&input); 15 | insta::assert_snapshot!(pretty_codegen(&interactive_clap_codegen)); 16 | 17 | let step_two_input = to_cli_args_structs_test_bridge::partial_output(&input) 18 | .unwrap_or_else(|err| panic!("couldn't parse syn::DeriveInput: {:?}", err)); 19 | 20 | let to_cli_args_codegen = crate::derives::to_cli_args::impl_to_cli_args(&step_two_input); 21 | insta::assert_snapshot!(pretty_codegen(&to_cli_args_codegen)); 22 | } 23 | 24 | #[test] 25 | fn test_simple_struct_with_named_arg() { 26 | let input = syn::parse_quote! { 27 | struct Account { 28 | #[interactive_clap(named_arg)] 29 | field_name: Sender, 30 | } 31 | }; 32 | 33 | let interactive_clap_codegen = crate::derives::interactive_clap::impl_interactive_clap(&input); 34 | insta::assert_snapshot!(pretty_codegen(&interactive_clap_codegen)); 35 | 36 | let step_two_input = to_cli_args_structs_test_bridge::partial_output(&input) 37 | .unwrap_or_else(|err| panic!("couldn't parse syn::DeriveInput: {:?}", err)); 38 | 39 | let to_cli_args_codegen = crate::derives::to_cli_args::impl_to_cli_args(&step_two_input); 40 | insta::assert_snapshot!(pretty_codegen(&to_cli_args_codegen)); 41 | } 42 | 43 | /// this tested this problem https://github.com/near/near-cli-rs/pull/444#issuecomment-2631866217 44 | #[test] 45 | fn test_bug_fix_of_to_cli_args_derive() { 46 | let input = syn::parse_quote! { 47 | pub struct ViewAccountSummary { 48 | /// What Account ID do you need to view? 49 | account_id: crate::types::account_id::AccountId, 50 | } 51 | }; 52 | 53 | let interactive_clap_codegen = crate::derives::interactive_clap::impl_interactive_clap(&input); 54 | insta::assert_snapshot!(pretty_codegen(&interactive_clap_codegen)); 55 | 56 | let step_two_input = to_cli_args_structs_test_bridge::partial_output(&input) 57 | .unwrap_or_else(|err| panic!("couldn't parse syn::DeriveInput: {:?}", err)); 58 | 59 | let to_cli_args_codegen = crate::derives::to_cli_args::impl_to_cli_args(&step_two_input); 60 | insta::assert_snapshot!(pretty_codegen(&to_cli_args_codegen)); 61 | } 62 | 63 | #[test] 64 | fn test_flag() { 65 | let input = syn::parse_quote! { 66 | struct Args { 67 | /// Offline mode 68 | #[interactive_clap(long)] 69 | offline: bool 70 | } 71 | }; 72 | 73 | let interactive_clap_codegen = crate::derives::interactive_clap::impl_interactive_clap(&input); 74 | insta::assert_snapshot!(pretty_codegen(&interactive_clap_codegen)); 75 | 76 | let step_two_input = to_cli_args_structs_test_bridge::partial_output(&input) 77 | .unwrap_or_else(|err| panic!("couldn't parse syn::DeriveInput: {:?}", err)); 78 | 79 | let to_cli_args_codegen = crate::derives::to_cli_args::impl_to_cli_args(&step_two_input); 80 | insta::assert_snapshot!(pretty_codegen(&to_cli_args_codegen)); 81 | } 82 | 83 | #[test] 84 | fn test_vec_multiple_opt() { 85 | let input = syn::parse_quote! { 86 | struct Args { 87 | #[interactive_clap(long_vec_multiple_opt)] 88 | pub env: Vec, 89 | } 90 | }; 91 | 92 | let interactive_clap_codegen = crate::derives::interactive_clap::impl_interactive_clap(&input); 93 | insta::assert_snapshot!(pretty_codegen(&interactive_clap_codegen)); 94 | 95 | let step_two_input = to_cli_args_structs_test_bridge::partial_output(&input) 96 | .unwrap_or_else(|err| panic!("couldn't parse syn::DeriveInput: {:?}", err)); 97 | 98 | let to_cli_args_codegen = crate::derives::to_cli_args::impl_to_cli_args(&step_two_input); 99 | insta::assert_snapshot!(pretty_codegen(&to_cli_args_codegen)); 100 | } 101 | 102 | #[test] 103 | // testing correct panic msg isn't really very compatible with 104 | // `proc-macro-error` crate 105 | #[should_panic] 106 | fn test_vec_multiple_opt_err() { 107 | let input = syn::parse_quote! { 108 | struct Args { 109 | #[interactive_clap(long_vec_multiple_opt)] 110 | pub env: String, 111 | } 112 | }; 113 | 114 | let interactive_clap_codegen = crate::derives::interactive_clap::impl_interactive_clap(&input); 115 | insta::assert_snapshot!(pretty_codegen(&interactive_clap_codegen)); 116 | } 117 | 118 | /// this test checks if doc comments are propagated up to `CliArgs` struct, 119 | /// which has `clap::Parser` derive on it 120 | /// 121 | /// also it checks that `#[interactive_clap(verbatim_doc_comment)]` attribute 122 | /// gets transferred to `#[clap(verbatim_doc_comment)]` on `second_field` of 123 | /// the same `CliArgs` struct 124 | #[test] 125 | fn test_doc_comments_propagate() { 126 | let input = syn::parse_quote! { 127 | struct Args { 128 | /// short first field description 129 | /// 130 | /// a longer paragraph, describing the usage and stuff with first field's 131 | /// awarenes of its possible applications 132 | #[interactive_clap(long)] 133 | #[interactive_clap(skip_interactive_input)] 134 | first_field: u64, 135 | /// short second field description 136 | /// 137 | /// a longer paragraph, describing the usage and stuff with second field's 138 | /// awareness of its possible applications 139 | #[interactive_clap(long)] 140 | #[interactive_clap(skip_interactive_input)] 141 | #[interactive_clap(verbatim_doc_comment)] 142 | second_field: String, 143 | /// short third field description 144 | /// 145 | /// a longer paragraph, describing the usage and stuff with third field's 146 | /// awareness of its possible applications 147 | #[interactive_clap(long)] 148 | #[interactive_clap(skip_interactive_input)] 149 | #[interactive_clap(verbatim_doc_comment)] 150 | third_field: bool, 151 | } 152 | }; 153 | 154 | let interactive_clap_codegen = crate::derives::interactive_clap::impl_interactive_clap(&input); 155 | insta::assert_snapshot!(pretty_codegen(&interactive_clap_codegen)); 156 | 157 | let step_two_input = to_cli_args_structs_test_bridge::partial_output(&input) 158 | .unwrap_or_else(|err| panic!("couldn't parse syn::DeriveInput: {:?}", err)); 159 | 160 | let to_cli_args_codegen = crate::derives::to_cli_args::impl_to_cli_args(&step_two_input); 161 | insta::assert_snapshot!(pretty_codegen(&to_cli_args_codegen)); 162 | } 163 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | # This specifies the version of Rust we use to build. 3 | # Individual crates in the workspace may support a lower version, as indicated by `rust-version` field in each crate's `Cargo.toml`. 4 | # The version specified below, should be at least as high as the maximum `rust-version` within the workspace. 5 | channel = "stable" 6 | components = ["rustfmt", "clippy", "rust-analyzer"] 7 | -------------------------------------------------------------------------------- /src/helpers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod snake_case_to_camel_case; 2 | pub mod to_kebab_case; 3 | -------------------------------------------------------------------------------- /src/helpers/snake_case_to_camel_case.rs: -------------------------------------------------------------------------------- 1 | pub fn snake_case_to_camel_case(s: String) -> String { 2 | let s_vec: Vec = s 3 | .to_lowercase() 4 | .split("_") 5 | .map(|s| s.replacen(&s[..1], &s[..1].to_ascii_uppercase(), 1)) 6 | .collect(); 7 | s_vec.join("") 8 | } 9 | -------------------------------------------------------------------------------- /src/helpers/to_kebab_case.rs: -------------------------------------------------------------------------------- 1 | pub fn to_kebab_case(s: String) -> String { 2 | let mut snake = String::new(); 3 | for (i, ch) in s.char_indices() { 4 | if i > 0 && ch.is_uppercase() { 5 | snake.push('-'); 6 | } 7 | snake.push(ch.to_ascii_lowercase()); 8 | } 9 | snake.as_str().replace("_", "-") 10 | } 11 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! The Interactive-clap library is an add-on for the Command Line Argument 2 | //! Parser ([`CLAP`](https://crates.io/crates/clap>)). Interactive-clap allows you to parse 3 | //! command line options. The peculiarity of this macro is that in the absence 4 | //! of command line parameters, the interactive mode of entering these data by 5 | //! the user is activated. 6 | 7 | pub use interactive_clap_derive::{InteractiveClap, ToCliArgs}; 8 | 9 | /// Associated type [`Self::CliVariant`] is defined during derive of 10 | /// [`macro@crate::InteractiveClap`] 11 | /// 12 | /// This type has derive of [`clap::Parser`](https://docs.rs/clap/4.5.24/clap/trait.Parser.html), which allows to parse 13 | /// initial input on cli, which may be incomplete 14 | pub trait ToCli { 15 | type CliVariant; 16 | } 17 | 18 | impl ToCli for String { 19 | type CliVariant = String; 20 | } 21 | 22 | impl ToCli for u128 { 23 | type CliVariant = u128; 24 | } 25 | 26 | impl ToCli for u64 { 27 | type CliVariant = u64; 28 | } 29 | 30 | impl ToCli for bool { 31 | type CliVariant = bool; 32 | } 33 | 34 | // TODO: the trait can clearly be shortened/renamed to `ContextScope` 35 | pub trait ToInteractiveClapContextScope { 36 | type InteractiveClapContextScope; 37 | } 38 | 39 | pub trait ToCliArgs { 40 | fn to_cli_args(&self) -> std::collections::VecDeque; 41 | } 42 | 43 | pub enum ResultFromCli { 44 | Ok(T), 45 | Cancel(Option), 46 | Back, 47 | Err(Option, E), 48 | } 49 | 50 | /// This trait drives the state machine of `interactive_clap` 51 | /// 52 | /// It selects next command variants with [inquire::Select](https://docs.rs/inquire/0.6.2/inquire/struct.Select.html) 53 | /// and prompts for non-optional arguments with [inquire::CustomType](https://docs.rs/inquire/0.6.2/inquire/struct.CustomType.html) 54 | pub trait FromCli { 55 | type FromCliContext; 56 | type FromCliError; 57 | fn from_cli( 58 | optional_clap_variant: Option<::CliVariant>, 59 | context: Self::FromCliContext, 60 | ) -> ResultFromCli<::CliVariant, Self::FromCliError> 61 | where 62 | Self: Sized + ToCli; 63 | } 64 | 65 | pub enum SelectVariantOrBack { 66 | Variant(T), 67 | Back, 68 | } 69 | impl std::fmt::Display for SelectVariantOrBack { 70 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 71 | if let Self::Variant(variant) = self { 72 | f.write_str(variant.get_message().unwrap()) 73 | } else { 74 | f.write_str("back") 75 | } 76 | } 77 | } 78 | --------------------------------------------------------------------------------