├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── cd.yml │ ├── ci.yml │ └── publish.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── crates ├── cli │ ├── Cargo.toml │ ├── README.md │ ├── src │ │ ├── app │ │ │ ├── busy.rs │ │ │ ├── error.rs │ │ │ ├── exit.rs │ │ │ └── mod.rs │ │ ├── commands │ │ │ ├── alias │ │ │ │ ├── action.rs │ │ │ │ ├── error.rs │ │ │ │ ├── help.rs │ │ │ │ ├── mod.rs │ │ │ │ └── storage │ │ │ │ │ ├── iter.rs │ │ │ │ │ ├── load.rs │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── show.rs │ │ │ │ │ └── store.rs │ │ │ ├── args.rs │ │ │ ├── debug.rs │ │ │ ├── http │ │ │ │ ├── help.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── output.rs │ │ │ │ ├── render │ │ │ │ │ ├── header.rs │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── request.rs │ │ │ │ │ └── response.rs │ │ │ │ └── version.rs │ │ │ └── mod.rs │ │ ├── core │ │ │ ├── error.rs │ │ │ ├── flags.rs │ │ │ ├── mod.rs │ │ │ ├── types.rs │ │ │ └── workspace.rs │ │ ├── items │ │ │ ├── mod.rs │ │ │ ├── number.rs │ │ │ ├── ser.rs │ │ │ └── value.rs │ │ ├── lib.rs │ │ ├── macros.rs │ │ ├── main.rs │ │ ├── parser │ │ │ ├── core.rs │ │ │ ├── error.rs │ │ │ ├── flags.rs │ │ │ ├── headers.rs │ │ │ ├── method.rs │ │ │ ├── mod.rs │ │ │ ├── normalizer.rs │ │ │ └── url.rs │ │ ├── request │ │ │ ├── body │ │ │ │ ├── form.rs │ │ │ │ ├── json.rs │ │ │ │ └── mod.rs │ │ │ ├── certificate.rs │ │ │ ├── header.rs │ │ │ ├── headers.rs │ │ │ └── mod.rs │ │ ├── shell │ │ │ ├── error.rs │ │ │ ├── form.rs │ │ │ ├── json.rs │ │ │ ├── mod.rs │ │ │ ├── os.rs │ │ │ └── stream.rs │ │ ├── test │ │ │ ├── alias.rs │ │ │ ├── mod.rs │ │ │ └── os.rs │ │ └── theme │ │ │ ├── default.rs │ │ │ ├── mod.rs │ │ │ └── style.rs │ └── tests │ │ ├── certificate.rs │ │ ├── macros.rs │ │ └── rh.rs └── test │ ├── Cargo.toml │ ├── README.md │ └── src │ └── lib.rs ├── doc ├── alias.md ├── authentication.md ├── contributing.md ├── examples.md ├── ginger-128.jpeg ├── help-and-version.md ├── install.md ├── json.md ├── rh-screencast.svg └── todo.md ├── rustfmt.toml └── snapcraft.yaml /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Execute 'rh ...' 16 | 2. See error 17 | 18 | **Expected behavior** 19 | A clear and concise description of what you expected to happen. 20 | 21 | **Screenshots** 22 | If applicable, add screenshots to help explain your problem. 23 | 24 | **Desktop or server (please complete the following information):** 25 | - OS: [e.g. Ubuntu Server 20.04 LTS] 26 | 27 | **Additional context** 28 | Add any other context about the problem here. 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | 3 | on: 4 | create: 5 | tags: 6 | - 'v[0-9]+.[0-9]+.[0-9]+*' 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | CARGO_TOML: "crates/cli/Cargo.toml" 11 | BIN_NAME: rh 12 | ARCHIVE_LINUX_EXT: .tar.gz 13 | ARCHIVE_WINDOWS_EXT: .zip 14 | 15 | jobs: 16 | version: 17 | name: Version 18 | runs-on: ubuntu-latest 19 | outputs: 20 | ARCHIVE_PREFIX_NAME: ${{ steps.ARCHIVE_PREFIX_NAME.outputs.value }} 21 | steps: 22 | - uses: actions/checkout@v2 23 | - id: RELEASE_VERSION 24 | run: echo "::set-output name=value::${GITHUB_REF#refs/*/}" 25 | - name: Check version 26 | if: ${{ !startsWith(steps.RELEASE_VERSION.outputs.value, 'v') }} 27 | run: exit 101 28 | - id: TAG_PKG_VERSION 29 | run: echo "::set-output name=value::$(echo ${{steps.RELEASE_VERSION.outputs.value}} | cut -c 2-)" 30 | 31 | - id: CARGO_PKG_VERSION 32 | run: echo "::set-output name=value::$(grep '^version' $CARGO_TOML | cut -f2 -d'"')" 33 | - name: Print versions 34 | run: | 35 | echo "TAG_PKG_VERSION....${{ steps.TAG_PKG_VERSION.outputs.value }}" 36 | echo "CARGO_PKG_VERSION..${{ steps.CARGO_PKG_VERSION.outputs.value }}" 37 | echo "...................${{ steps.TAG_PKG_VERSION.outputs.value != steps.CARGO_PKG_VERSION.outputs.value }}" 38 | echo "RELEASE_VERSION....${{ steps.RELEASE_VERSION.outputs.value }}" 39 | - name: Check versions 40 | if: ${{ steps.TAG_PKG_VERSION.outputs.value != steps.CARGO_PKG_VERSION.outputs.value }} 41 | run: exit 102 42 | 43 | - id: ARCHIVE_PREFIX_NAME 44 | run: echo "::set-output name=value::$BIN_NAME-${{steps.RELEASE_VERSION.outputs.value}}" 45 | 46 | build: 47 | name: Build (${{ matrix.os }}) 48 | needs: [version] 49 | runs-on: ${{ matrix.os }} 50 | 51 | strategy: 52 | fail-fast: false 53 | matrix: 54 | include: 55 | - { plat: x86_64, os: ubuntu-20.04, target: x86_64-unknown-linux-gnu, strip: strip} 56 | - { plat: x86_64, os: ubuntu-20.04, target: x86_64-unknown-linux-musl, strip: strip, cross: true} 57 | - { plat: arm_64, os: ubuntu-20.04, target: aarch64-unknown-linux-gnu, strip: aarch64-linux-gnu-strip, cross: true} 58 | - { plat: x86_32, os: ubuntu-20.04, target: i686-unknown-linux-musl, strip: strip, cross: true} 59 | - { plat: x86_32, os: ubuntu-20.04, target: i686-unknown-linux-gnu, strip: strip, cross: true} 60 | - { plat: arm_32, os: ubuntu-20.04, target: arm-unknown-linux-musleabihf, strip: arm-linux-gnueabihf-strip, cross: true} 61 | - { plat: arm_32, os: ubuntu-20.04, target: arm-unknown-linux-gnueabihf, strip: arm-linux-gnueabihf-strip, cross: true} 62 | - { plat: x86_64, os: macos-latest, target: x86_64-apple-darwin, strip: strip } 63 | - { plat: x86_64, os: windows-2019, target: x86_64-pc-windows-gnu } 64 | - { plat: x86_64, os: windows-2019, target: x86_64-pc-windows-msvc } 65 | - { plat: x86_32, os: windows-2019, target: i686-pc-windows-msvc } 66 | 67 | steps: 68 | - name: Checkout 69 | uses: actions/checkout@v2 70 | 71 | - name: Toolchain 72 | uses: actions-rs/toolchain@v1 73 | with: 74 | profile: minimal 75 | toolchain: stable 76 | target: ${{ matrix.target }} 77 | override: true 78 | 79 | - name: Build 80 | uses: actions-rs/cargo@v1 81 | with: 82 | use-cross: ${{ matrix.cross }} 83 | command: build 84 | args: --release --features alias --target ${{ matrix.target }} 85 | 86 | - name: Target directory 87 | shell: bash 88 | run: echo "BIN_DIR=target/${{ matrix.target }}/release" >> $GITHUB_ENV 89 | 90 | - name: Strip 91 | if: ${{ matrix.strip == 'aarch64-linux-gnu-strip' }} 92 | run: sudo apt-get install binutils-aarch64-linux-gnu 93 | - name: Strip 94 | if: ${{ matrix.strip == 'arm-linux-gnueabihf-strip' }} 95 | run: sudo apt-get install binutils-arm-linux-gnueabihf 96 | - name: Strip 97 | if: ${{ matrix.strip }} 98 | run: ${{ matrix.strip }} $BIN_DIR/$BIN_NAME 99 | 100 | - name: Archive 101 | shell: bash 102 | run: | 103 | archive_name=${{needs.version.outputs.ARCHIVE_PREFIX_NAME}}-${{ matrix.target }} 104 | archive_dir=$archive_name 105 | mkdir -p $archive_dir 106 | 107 | if [ "${{ matrix.os }}" = "windows-2019" ]; then 108 | cp $BIN_DIR/$BIN_NAME.exe $archive_dir 109 | archive_filename="$archive_name$ARCHIVE_WINDOWS_EXT" 110 | 7z a "$archive_filename" "$archive_dir" 111 | echo "RELEASE_ASSET=$archive_filename" >> $GITHUB_ENV 112 | else 113 | cp $BIN_DIR/$BIN_NAME $archive_dir 114 | archive_filename="$archive_name$ARCHIVE_LINUX_EXT" 115 | tar cvfz $archive_filename $archive_dir 116 | echo "RELEASE_ASSET=$archive_filename" >> $GITHUB_ENV 117 | fi 118 | 119 | - name: Release 120 | uses: softprops/action-gh-release@v1 121 | with: 122 | files: | 123 | ${{ env.RELEASE_ASSET }} 124 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | paths: 6 | - '**.rs' 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | 11 | jobs: 12 | tests: 13 | name: ${{ matrix.os }} 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | os: [ubuntu-latest, macos-latest, windows-latest] 19 | rust: [stable] 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v2 23 | 24 | - name: Lint 25 | if: matrix.os == 'ubuntu-latest' 26 | run: cargo clippy --features alias -- -D warnings 27 | 28 | - name: Format 29 | continue-on-error: true 30 | if: matrix.os == 'ubuntu-latest' 31 | run: cargo fmt -- --check 32 | 33 | - name: Build 34 | run: cargo build --verbose --features alias 35 | 36 | - name: Unit tests 37 | run: cargo test --verbose --features alias -- --test-threads=1 38 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | workflow_run: 5 | workflows: [CD] 6 | types: 7 | - completed 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | on-failure: 14 | runs-on: ubuntu-latest 15 | if: ${{ github.event.workflow_run.conclusion == 'failure' }} 16 | steps: 17 | - run: echo 'The triggering workflow failed' 18 | - run: exit 100 19 | 20 | crates: 21 | name: Crates.io 22 | runs-on: ubuntu-latest 23 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v2 27 | 28 | - name: Publish 29 | env: 30 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 31 | run: cargo publish --package rh 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /tmp 3 | /data 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "crates/cli", 5 | "crates/test", 6 | ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 twigly 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 | [![CI](https://github.com/twigly/rh/actions/workflows/ci.yml/badge.svg)](https://github.com/twigly/rh/actions/workflows/ci.yml) 2 | [![CD](https://github.com/twigly/rh/actions/workflows/cd.yml/badge.svg)](https://github.com/twigly/rh/actions/workflows/cd.yml) 3 | 4 | # Rust HTTP Cli 5 | 6 | The command name in your terminal is ```rh```. 7 | 8 | # rh: user-friendly command-line HTTP client 9 | 10 | ```rh``` is a user-friendly, lightweight and performant command-line tool to request HTTP APis. You can debug, test and verify any HTTP APi with ```rh``` in a simple and efficient way. ```rh``` is focused on performance and stability. You don't need OpenSSL because ```rh``` is based on Rustls, a modern TLS library alternative to OpenSSL. 11 | 12 | ```rh``` is a standalone application with no runtime or garbage collector, so it doesn't require Python or Java to be installed on your machine, for example. ```rh``` is based on [Rust,](https://www.rust-lang.org) which is a blazing fast and memory-efficient language. 13 | 14 | The name ```rh``` stands for Rust HTTP. 15 | 16 | 17 | 18 | # Getting started 19 | 20 | → [Installation guide](doc/install.md) 21 | 22 | → [Contributing guide](doc/contributing.md) 23 | 24 | # Features 25 | 26 | You can use ```rh``` right now, and some new features are coming soon. New features will be based on user request (please [file an issue](https://github.com/twigly/rh/issues) to make suggestions/requests.) 27 | 28 | - [X] Simple syntax to be more intuitive 29 | - [X] Easy file download & upload 30 | - [X] JSON made simple for command-line 31 | - [X] JSON-friendly 32 | - [X] Headers made simple for command-line 33 | - [X] Self-signed SSL certificates 34 | - [X] Don't repeat yourself with [aliases](doc/alias.md) 35 | - [ ] Package manager 36 | - [ ] Multi URLs 37 | - [ ] Better help & version ([help & version](doc/help-and-version.md)) 38 | - More [to do](doc/todo.md) 39 | 40 | 41 | # Don't repeat yourself 42 | 43 | If you frequently execute the same requests, ```rh``` can save you time. An **alias** helps to change default values or create shortcuts. You can predefine what you like: it could be just the headers, or it could be everything. 44 | 45 | For example, someone could create an alias ```mp1-status``` (that would stand for "my-project-1", for example). Let's say you want to execute the following command on a regular basis: 46 | 47 | ```bash 48 | > rh http://local-dev-mp1/status -UHhc X-Custom-Header:My-app 49 | ``` 50 | 51 | ```-UHhc``` to show the ```-U```RL and the method + to show the request ```-H```eaders + to show the response ```-h```eaders + to show a ```-c```ompact response 52 | 53 | ```bash 54 | > rh alias @mp1-status http://local-dev-mp1/status -UHhc X-Custom-Header:My-app 55 | ``` 56 | 57 | So now, you can reuse this config: 58 | 59 | ```bash 60 | > rh @mp1-status 61 | ``` 62 | 63 | → [See more about aliases](doc/alias.md) 64 | 65 | # Examples 66 | 67 | Who doesn't like "Hello, World!": 68 | 69 | ```bash 70 | > rh httpbin.org/get 71 | ``` 72 | 73 | Change the method: 74 | 75 | ```bash 76 | > rh HEAD https://httpbin.org/anything 77 | ``` 78 | 79 | Localhost with a particular port: 80 | 81 | ```bash 82 | > rh :9200 83 | ``` 84 | 85 | You can POST data as JSON (it's the default format, see [more about it](doc/json.md)): 86 | 87 | ```bash 88 | > rh https://httpbin.org/anything X-App:Super1 item1=Hello item2=World 89 | ``` 90 | 91 | You can POST data using the URL encoded format: 92 | 93 | ```bash 94 | > rh https://httpbin.org/anything key1=1 --form 95 | ``` 96 | 97 | You can POST raw data: 98 | 99 | ```bash 100 | > rh https://httpbin.org/anything --raw=hello 101 | ``` 102 | 103 | You can download a file and save it: 104 | 105 | ```bash 106 | > rh https://httpbin.org/image/jpeg > image.jpeg 107 | ``` 108 | 109 | → [More examples](doc/examples.md) 110 | 111 | # License 112 | 113 | ```rh``` is distributed under the terms of the MIT license. See [LICENSE](/LICENSE) for details. 114 | 115 | # Contributing 116 | 117 | If you are interested in contributing to the ```rh``` project, please take a look at the [contributing guide](doc/contributing.md). If you'd like to request a feature or report a bug, please create a [GitHub issue](https://github.com/twigly/rh/issues). 118 | 119 | Thanks to everyone developing the third party libraries used in this project. 120 | -------------------------------------------------------------------------------- /crates/cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rh" 3 | version = "0.1.14" 4 | edition = "2021" 5 | authors = ["twigly"] 6 | license = "MIT" 7 | description = "A user-friendly command-line tool to request HTTP APis" 8 | readme = "README.md" 9 | homepage = "https://github.com/twigly/rh" 10 | repository = "https://github.com/twigly/rh" 11 | keywords = ["cli", "http", "terminal", "tool", "devops"] 12 | categories = ["command-line-utilities"] 13 | 14 | [dependencies] 15 | ansi_term = "0.12" 16 | atty = "0.2.14" 17 | colored_json = "2" 18 | content_inspector = "0.2.4" 19 | dirs = "4.0" 20 | indicatif = "0.16" 21 | regex = "1.5.4" 22 | reqwest = { version = "0.11", default-features = false, features = ["blocking", "cookies", "gzip", "rustls-tls"] } 23 | serde = { version = "1.0", features = ["derive"] } 24 | serde_urlencoded = "0.7.1" 25 | serde_json = { version = "1.0", features = ["preserve_order"] } 26 | termsize = "0.1" 27 | url = "2.2.2" 28 | # wild = { version = "2.0", optional = true } 29 | 30 | [dev-dependencies] 31 | httpmock = "0.6" 32 | rh_test = { path = "../test" } 33 | 34 | [features] 35 | spinner = [] 36 | alias = [] 37 | -------------------------------------------------------------------------------- /crates/cli/README.md: -------------------------------------------------------------------------------- 1 | [![CI](https://github.com/twigly/rh/actions/workflows/ci.yml/badge.svg)](https://github.com/twigly/rh/actions/workflows/ci.yml) 2 | [![CD](https://github.com/twigly/rh/actions/workflows/cd.yml/badge.svg)](https://github.com/twigly/rh/actions/workflows/cd.yml) 3 | 4 | # Rust HTTP Cli 5 | 6 | The command name in your terminal is ```rh```. 7 | 8 | # rh: user-friendly command-line HTTP client 9 | 10 | ```rh``` is a user-friendly, lightweight and performant command-line tool to request HTTP APis. You can debug, test and verify any HTTP APi with ```rh``` in a simple and efficient way. ```rh``` is focused on performance and stability. You don't need OpenSSL because ```rh``` is based on Rustls, a modern TLS library alternative to OpenSSL. 11 | 12 | ```rh``` is a standalone application with no runtime or garbage collector, so it doesn't require Python or Java to be installed on your machine, for example. ```rh``` is based on [Rust,](https://www.rust-lang.org) which is a blazing fast and memory-efficient language. 13 | 14 | The name ```rh``` stands for Rust HTTP. 15 | 16 | 17 | 18 | # Getting started 19 | 20 | → [Installation guide](../../doc/install.md) 21 | 22 | → [Contributing guide](../../doc/contributing.md) 23 | 24 | # Features 25 | 26 | You can use ```rh``` right now, and some new features are coming soon. New features will be based on user request (please [file an issue](https://github.com/twigly/rh/issues) to make suggestions/requests.) 27 | 28 | - [X] Simple syntax to be more intuitive 29 | - [X] Easy file download & upload 30 | - [X] JSON made simple for command-line 31 | - [X] JSON-friendly 32 | - [X] Headers made simple for command-line 33 | - [X] Self-signed SSL certificates 34 | - [X] Don't repeat yourself with [aliases](../../doc/alias.md) 35 | - [ ] Package manager 36 | - [ ] Multi URLs 37 | - [ ] Better help & version ([help & version](../../doc/help-and-version.md)) 38 | - More [to do](../../doc/todo.md) 39 | 40 | 41 | # Don't repeat yourself 42 | 43 | If you frequently execute the same requests, ```rh``` can save you time. An **alias** helps to change default values or create shortcuts. You can predefine what you like: it could be just the headers, or it could be everything. 44 | 45 | For example, someone could create an alias ```mp1-status``` (that would stand for "my-project-1", for example). Let's say you want to execute the following command on a regular basis: 46 | 47 | ```bash 48 | > rh http://local-dev-mp1/status -UHhc X-Custom-Header:My-app 49 | ``` 50 | 51 | ```-UHhc``` to show the ```-U```RL and the method + to show the request ```-H```eaders + to show the response ```-h```eaders + to show a ```-c```ompact response 52 | 53 | ```bash 54 | > rh alias @mp1-status http://local-dev-mp1/status -UHhc X-Custom-Header:My-app 55 | ``` 56 | 57 | So now, you can reuse this config: 58 | 59 | ```bash 60 | > rh @mp1-status 61 | ``` 62 | 63 | → [See more about aliases](../../doc/alias.md) 64 | 65 | # Examples 66 | 67 | Who doesn't like "Hello, World!": 68 | 69 | ```bash 70 | > rh httpbin.org/get 71 | ``` 72 | 73 | Change the method: 74 | 75 | ```bash 76 | > rh HEAD https://httpbin.org/anything 77 | ``` 78 | 79 | Localhost with a particular port: 80 | 81 | ```bash 82 | > rh :9200 83 | ``` 84 | 85 | You can POST data as JSON (it's the default format, see [more about it](../../doc/json.md)): 86 | 87 | ```bash 88 | > rh https://httpbin.org/anything X-App:Super1 item1=Hello item2=World 89 | ``` 90 | 91 | You can POST data using the URL encoded format: 92 | 93 | ```bash 94 | > rh https://httpbin.org/anything key1=1 --form 95 | ``` 96 | 97 | You can POST raw data: 98 | 99 | ```bash 100 | > rh https://httpbin.org/anything --raw=hello 101 | ``` 102 | 103 | You can download a file and save it: 104 | 105 | ```bash 106 | > rh https://httpbin.org/image/jpeg > image.jpeg 107 | ``` 108 | 109 | → [More examples](../../doc/examples.md) 110 | 111 | # License 112 | 113 | ```rh``` is distributed under the terms of the MIT license. See [LICENSE](/LICENSE) for details. 114 | 115 | # Contributing 116 | 117 | If you are interested in contributing to the ```rh``` project, please take a look at the [contributing guide](../../doc/contributing.md). If you'd like to request a feature or report a bug, please create a [GitHub issue](https://github.com/twigly/rh/issues). 118 | 119 | Thanks to everyone developing the third party libraries used in this project. 120 | -------------------------------------------------------------------------------- /crates/cli/src/app/busy.rs: -------------------------------------------------------------------------------- 1 | use indicatif::{ProgressBar, ProgressStyle}; 2 | 3 | #[derive(Clone)] 4 | pub struct Spinner { 5 | pb: ProgressBar, 6 | } 7 | 8 | impl Spinner { 9 | pub fn new() -> Spinner { 10 | let pb = ProgressBar::new_spinner(); 11 | pb.enable_steady_tick(120); 12 | pb.set_style( 13 | ProgressStyle::default_spinner() 14 | .tick_strings(&[". ", ".. ", "...", " ..", " .", " ..", "...", ".. "]) 15 | .template("{msg} {spinner}"), 16 | ); 17 | pb.set_message("Running"); 18 | Spinner { pb } 19 | } 20 | 21 | pub fn done(self) { 22 | self.pb.finish_and_clear(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /crates/cli/src/app/error.rs: -------------------------------------------------------------------------------- 1 | use super::Error; 2 | use crate::rh_name; 3 | use crate::shell::os::OsDirs; 4 | use crate::shell::{error::ErrorRender, Shell}; 5 | use std::fmt; 6 | use std::io::Write; 7 | 8 | pub fn show(shell: &mut Shell, err: &Error) { 9 | let rf = ErrorRender::new(err); 10 | let res = shell.err(rf); 11 | match res { 12 | Ok(_) => {} 13 | Err(err) => { 14 | let _ = writeln!(std::io::stderr(), "{}", err); 15 | } 16 | } 17 | } 18 | 19 | impl fmt::Display for Error { 20 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 21 | match self { 22 | Error::NoArgs => write!(f, "try '{} --help' for more information.", rh_name!()), 23 | Error::MissingUrl => write!(f, "no URL specified."), 24 | Error::ItemsAndRawMix => write!(f, "not possible to mix raw data and key=value items."), 25 | Error::TooManyRaw => write!(f, "only one raw data item is allowed."), 26 | Error::ContradictoryScheme => write!(f, "either http or https."), 27 | Error::Unexpected(err) => write!(f, "found argument '{}' which wasn't expected, or isn't valid in this context.", err), 28 | Error::InvalidFlag(args) => write!(f, "found argument '{}' which wasn't expected, or isn't valid in this context.", args), 29 | Error::InvalidHeader(err) => write!(f, "invalid header '{}'.", err), 30 | Error::InvalidItem(err) => write!(f, "invalid item '{}'.", err), 31 | Error::BadHeaderName(_) => write!(f, "invalid header name."), 32 | Error::BadHeaderValue(_) => write!(f, "invalid header value."), 33 | Error::Request(err) => write!(f, "{}", err), 34 | Error::Io(err) => write!(f, "{}", err), 35 | #[cfg(feature = "alias")] 36 | Error::AliasCommand(err) => { 37 | writeln!(f, "the alias subcommand failed, {}", err)?; 38 | write!(f, "try '{} {} --help' for more information.", rh_name!(), crate::commands::alias::COMMAND_ALIAS) 39 | } 40 | #[cfg(feature = "alias")] 41 | Error::Alias(err) => { 42 | writeln!(f, "cannot find the alias '{}'", err)?; 43 | write!(f, "try '{} {} --help' for more information.", rh_name!(), crate::commands::alias::COMMAND_ALIAS) 44 | } 45 | #[cfg(feature = "alias")] 46 | Error::AliasOther => { 47 | // FIXME To be removed 48 | writeln!(f, "unknown error with alias")?; 49 | write!(f, "try '{} {} --help' for more information.", rh_name!(), crate::commands::alias::COMMAND_ALIAS) 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /crates/cli/src/app/exit.rs: -------------------------------------------------------------------------------- 1 | use super::Error; 2 | 3 | pub const SUCCESS: i32 = 0; 4 | 5 | pub fn code_on_success() -> i32 { 6 | SUCCESS 7 | } 8 | 9 | pub fn code_on_error(err: Error) -> i32 { 10 | // FIXME All errors must have an exit code 11 | match err { 12 | Error::NoArgs => 100, 13 | Error::MissingUrl => 101, 14 | Error::ItemsAndRawMix => 200, 15 | Error::TooManyRaw => 201, 16 | Error::ContradictoryScheme => 301, 17 | #[cfg(feature = "alias")] 18 | Error::AliasCommand(_) => 950, 19 | #[cfg(feature = "alias")] 20 | Error::Alias(_) => 900, 21 | _ => 999, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /crates/cli/src/app/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "spinner")] 2 | mod busy; 3 | mod error; 4 | mod exit; 5 | 6 | use crate::commands::ArgsCommand; 7 | use crate::core::{Args, Error, Result}; 8 | use crate::shell::os::OsDirs; 9 | use crate::shell::Shell; 10 | use std::io::Write; 11 | 12 | #[cfg(feature = "spinner")] 13 | use busy::Spinner; 14 | 15 | pub struct App<'a, OD, O, E> { 16 | shell: &'a mut Shell<'a, OD, O, E>, 17 | #[cfg(feature = "spinner")] 18 | busy: Spinner, 19 | } 20 | 21 | impl<'a, OD: OsDirs, O: Write, E: Write> App<'a, OD, O, E> { 22 | pub fn new(shell: &'a mut Shell<'a, OD, O, E>) -> Self { 23 | Self { 24 | shell, 25 | #[cfg(feature = "spinner")] 26 | busy: busy::Spinner::new(), 27 | } 28 | } 29 | 30 | pub fn exit_code(self, err: Option) -> i32 { 31 | #[cfg(feature = "spinner")] 32 | self.busy.done(); 33 | 34 | match err { 35 | Some(err) => { 36 | error::show(self.shell, &err); 37 | exit::code_on_error(err) 38 | } 39 | None => exit::code_on_success(), 40 | } 41 | } 42 | 43 | pub fn run(&mut self, args: &mut Args) -> Result<()> { 44 | let command = args.command(self.shell.os_dirs())?; 45 | command.execute(self.shell, args, || {})?; 46 | 47 | #[cfg(feature = "spinner")] 48 | self.busy.clone().done(); 49 | 50 | // self.shell.flush()?; 51 | Ok(()) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /crates/cli/src/commands/alias/action.rs: -------------------------------------------------------------------------------- 1 | use super::{Error, Result}; 2 | use crate::core::Args; 3 | 4 | #[cfg_attr(test, derive(Debug, PartialEq))] 5 | pub enum Action { 6 | Add, 7 | Delete, 8 | List, 9 | Help, 10 | } 11 | 12 | type IsValidAliasPtr = fn(arg: Option<&String>) -> bool; 13 | type FixAliasPtr = fn(alias_name: &str) -> String; 14 | 15 | pub fn get(args: &mut Args, is_valid_alias: IsValidAliasPtr, fix_alias: FixAliasPtr) -> Result { 16 | if let Some(potential_subcommand) = args.first() { 17 | if is_delete(potential_subcommand, args, is_valid_alias, fix_alias)? { 18 | args.remove(0); 19 | return Ok(Action::Delete); 20 | } else if is_list(potential_subcommand, args)? { 21 | args.remove(0); 22 | return Ok(Action::List); 23 | } else if is_add(potential_subcommand, args, is_valid_alias)? { 24 | args.remove(0); 25 | return Ok(Action::Add); 26 | } else if is_help(potential_subcommand)? { 27 | args.remove(0); 28 | return Ok(Action::Help); 29 | } 30 | } 31 | 32 | if args.is_empty() || (args.len() == 1 && is_valid_alias(args.get(0))) { 33 | Err(Error::NoArgs) 34 | } else { 35 | Ok(Action::Add) 36 | } 37 | } 38 | 39 | fn is_help(potential_subcommand: &str) -> Result { 40 | Ok(potential_subcommand == "--help" || potential_subcommand == "-h") 41 | } 42 | fn is_add(potential_subcommand: &str, args: &Args, is_valid_alias: IsValidAliasPtr) -> Result { 43 | if potential_subcommand == "--add" { 44 | if args.len() == 1 || (args.len() == 2 && is_valid_alias(args.get(1))) { 45 | return Err(Error::MissingArgsForAdd); 46 | } else { 47 | return Ok(true); 48 | } 49 | } 50 | Ok(false) 51 | } 52 | fn is_delete(potential_subcommand: &str, args: &Args, is_valid_alias: IsValidAliasPtr, fix_alias: FixAliasPtr) -> Result { 53 | if potential_subcommand == "--delete" || potential_subcommand == "--del" { 54 | if args.len() == 1 || (args.len() == 2 && is_valid_alias(args.get(1))) { 55 | Ok(true) 56 | } else { 57 | let first_arg = args.get(1).unwrap().to_lowercase(); 58 | Err(Error::TooManyArgsForDelete(fix_alias(&first_arg))) 59 | } 60 | } else { 61 | Ok(false) 62 | } 63 | } 64 | fn is_list(potential_subcommand: &str, args: &Args) -> Result { 65 | if potential_subcommand == "--list" { 66 | if args.len() == 1 { 67 | Ok(true) 68 | } else { 69 | Err(Error::TooManyArgsForList) 70 | } 71 | } else { 72 | Ok(false) 73 | } 74 | } 75 | 76 | // UNIT TESTS ///////////////////////////////////////////////////////////////////////////// 77 | 78 | #[cfg(test)] 79 | mod tests { 80 | use super::*; 81 | use crate::commands::ALIAS_NAME_PREFIX; 82 | 83 | fn is_valid(arg: Option<&String>) -> bool { 84 | match arg { 85 | Some(arg) => arg.starts_with(ALIAS_NAME_PREFIX), 86 | None => false, 87 | } 88 | } 89 | 90 | fn fix_alias_name(alias_name: &str) -> String { 91 | if let Some(alias_name) = alias_name.strip_prefix(ALIAS_NAME_PREFIX) { 92 | alias_name.to_string() 93 | } else { 94 | alias_name.to_string() 95 | } 96 | } 97 | 98 | #[test] 99 | fn no_subcommand() { 100 | let mut args = rh_test::args![]; 101 | let res = get(&mut args, is_valid, fix_alias_name); 102 | assert!(res.is_err()); 103 | assert_eq!(res.unwrap_err(), Error::NoArgs); 104 | } 105 | 106 | #[test] 107 | fn invalid_add_subcommand_default_alias() { 108 | let mut args = rh_test::args!["--add"]; 109 | let res = get(&mut args, is_valid, fix_alias_name); 110 | assert!(res.is_err()); 111 | assert_eq!(res.unwrap_err(), Error::MissingArgsForAdd); 112 | } 113 | 114 | #[test] 115 | fn invalid_add_subcommand_custom_alias() { 116 | let mut args = rh_test::args!["--add", rh_test::arg_alias!("an-alias")]; 117 | let res = get(&mut args, is_valid, fix_alias_name); 118 | assert!(res.is_err()); 119 | assert_eq!(res.unwrap_err(), Error::MissingArgsForAdd); 120 | } 121 | 122 | #[test] 123 | fn add_subcommand_default_alias() { 124 | let mut args = rh_test::args!["--add", "-v"]; 125 | let subcommand = get(&mut args, is_valid, fix_alias_name).unwrap(); 126 | assert_eq!(subcommand, Action::Add); 127 | 128 | let mut args = rh_test::args!["whatever"]; 129 | let subcommand = get(&mut args, is_valid, fix_alias_name).unwrap(); 130 | assert_eq!(subcommand, Action::Add); 131 | } 132 | 133 | #[test] 134 | fn add_subcommand_custom_alias() { 135 | let mut args = rh_test::args!["--add", rh_test::arg_alias!("an-alias"), "-v"]; 136 | let subcommand = get(&mut args, is_valid, fix_alias_name).unwrap(); 137 | assert_eq!(subcommand, Action::Add); 138 | 139 | let mut args = rh_test::args![rh_test::arg_alias!("an-alias"), "whatever"]; 140 | let subcommand = get(&mut args, is_valid, fix_alias_name).unwrap(); 141 | assert_eq!(subcommand, Action::Add); 142 | } 143 | 144 | #[test] 145 | fn invalid_delete_subcommand_uppercase() { 146 | let mut args = rh_test::args!["--delete", "arg-ABC"]; 147 | let res = get(&mut args, is_valid, fix_alias_name); 148 | assert!(res.is_err()); 149 | assert_eq!(res.unwrap_err(), Error::TooManyArgsForDelete("arg-abc".into())); 150 | } 151 | #[test] 152 | fn invalid_delete_subcommand_lowercase() { 153 | let mut args = rh_test::args!["--del", "arg-123"]; 154 | let res = get(&mut args, is_valid, fix_alias_name); 155 | assert!(res.is_err()); 156 | assert_eq!(res.unwrap_err(), Error::TooManyArgsForDelete("arg-123".into())); 157 | } 158 | 159 | #[test] 160 | fn invalid_delete_subcommand_error_without_prefix() { 161 | let mut args = rh_test::args!["--del", rh_test::arg_alias!("an-alias"), "arg-123"]; 162 | let res = get(&mut args, is_valid, fix_alias_name); 163 | assert!(res.is_err()); 164 | assert_eq!(res.unwrap_err(), Error::TooManyArgsForDelete("an-alias".into())); 165 | } 166 | 167 | #[test] 168 | fn delete_subcommand_default_alias() { 169 | let mut args = rh_test::args!["--delete"]; 170 | let subcommand = get(&mut args, is_valid, fix_alias_name).unwrap(); 171 | assert_eq!(subcommand, Action::Delete); 172 | 173 | let mut args = rh_test::args!["--del"]; 174 | let subcommand = get(&mut args, is_valid, fix_alias_name).unwrap(); 175 | assert_eq!(subcommand, Action::Delete); 176 | } 177 | 178 | #[test] 179 | fn delete_subcommand_custom_alias() { 180 | let mut args = rh_test::args!["--delete", rh_test::arg_alias!("an-alias")]; 181 | let subcommand = get(&mut args, is_valid, fix_alias_name).unwrap(); 182 | assert_eq!(subcommand, Action::Delete); 183 | 184 | let mut args = rh_test::args!["--del", rh_test::arg_alias!("an-alias")]; 185 | let subcommand = get(&mut args, is_valid, fix_alias_name).unwrap(); 186 | assert_eq!(subcommand, Action::Delete); 187 | } 188 | 189 | #[test] 190 | fn invalid_list_subcommand() { 191 | let mut args = rh_test::args!["--list", "too-many-args"]; 192 | let res = get(&mut args, is_valid, fix_alias_name); 193 | assert!(res.is_err()); 194 | assert_eq!(res.unwrap_err(), Error::TooManyArgsForList); 195 | } 196 | 197 | #[test] 198 | fn list_subcommand() { 199 | let mut args = rh_test::args!["--list"]; 200 | let subcommand = get(&mut args, is_valid, fix_alias_name).unwrap(); 201 | assert_eq!(subcommand, Action::List); 202 | } 203 | 204 | #[test] 205 | fn help_subcommand_strict() { 206 | let mut args = rh_test::args!["--help"]; 207 | let subcommand = get(&mut args, is_valid, fix_alias_name).unwrap(); 208 | assert_eq!(subcommand, Action::Help); 209 | 210 | let mut args = rh_test::args!["-h"]; 211 | let subcommand = get(&mut args, is_valid, fix_alias_name).unwrap(); 212 | assert_eq!(subcommand, Action::Help); 213 | } 214 | 215 | #[test] 216 | fn help_subcommand_flexible() { 217 | let mut args = rh_test::args!["--help", "blabla"]; 218 | let subcommand = get(&mut args, is_valid, fix_alias_name).unwrap(); 219 | assert_eq!(subcommand, Action::Help); 220 | 221 | let mut args = rh_test::args!["-h", "blabla"]; 222 | let subcommand = get(&mut args, is_valid, fix_alias_name).unwrap(); 223 | assert_eq!(subcommand, Action::Help); 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /crates/cli/src/commands/alias/error.rs: -------------------------------------------------------------------------------- 1 | use crate::commands::alias::COMMAND_ALIAS; 2 | use crate::commands::ALIAS_NAME_PREFIX; 3 | use crate::core::Error as CoreError; 4 | use crate::rh_name; 5 | use std::fmt; 6 | use std::io; 7 | use std::io::ErrorKind as IoErrorKind; 8 | 9 | #[cfg_attr(test, derive(Debug))] 10 | #[derive(PartialEq)] 11 | pub enum Error { 12 | CannotCreateAlias(String, ErrorKind), 13 | CannotLoadAlias(String, ErrorKind), 14 | CannotDeleteAlias(String, ErrorKind), 15 | CannotListAlias, 16 | Io(ErrorKind), 17 | NoArgs, 18 | MissingArgsForAdd, 19 | TooManyArgsForDelete(String), 20 | TooManyArgsForList, 21 | } 22 | 23 | impl From for CoreError { 24 | fn from(err: Error) -> CoreError { 25 | CoreError::AliasCommand(err) 26 | } 27 | } 28 | 29 | impl From for Error { 30 | fn from(err: io::Error) -> Error { 31 | Error::Io(err.kind().into()) 32 | } 33 | } 34 | 35 | impl fmt::Display for Error { 36 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 37 | match self { 38 | Error::CannotCreateAlias(alias_name, err) => { 39 | write!(f, "{} (alias name {}{})", err.as_str(), ALIAS_NAME_PREFIX, alias_name) 40 | } 41 | Error::CannotLoadAlias(alias_name, err) => { 42 | write!(f, "{} (alias name {}{})", err.as_str(), ALIAS_NAME_PREFIX, alias_name) 43 | } 44 | Error::CannotDeleteAlias(alias_name, err) => { 45 | write!(f, "{} (alias name {}{})", err.as_str(), ALIAS_NAME_PREFIX, alias_name) 46 | } 47 | Error::CannotListAlias => write!(f, "cannot list aliases"), 48 | Error::Io(err) => write!(f, "{}", err.as_str()), 49 | Error::NoArgs => write!(f, "missing arguments"), 50 | Error::MissingArgsForAdd => write!(f, "missing arguments for the --add command"), 51 | Error::TooManyArgsForDelete(first_arg) => { 52 | write!( 53 | f, 54 | "too many arguments for the --delete command\ndid you mean: {} {} --delete {}{}", 55 | rh_name!(), 56 | COMMAND_ALIAS, 57 | ALIAS_NAME_PREFIX, 58 | first_arg 59 | ) 60 | } 61 | Error::TooManyArgsForList => { 62 | write!(f, "too many arguments for the --list command") 63 | } 64 | } 65 | } 66 | } 67 | 68 | #[cfg_attr(test, derive(Debug))] 69 | #[derive(PartialEq)] 70 | pub enum ErrorKind { 71 | ConfigDirectoryNotFound, 72 | InvalidConfigDirectory, 73 | CannotCreateAppConfigDirectory, 74 | AliasFileNotFound, 75 | AliasFilePermissionDenied, 76 | Unknown, 77 | } 78 | 79 | impl ErrorKind { 80 | pub(crate) fn as_str(&self) -> &'static str { 81 | use ErrorKind::*; 82 | match *self { 83 | ConfigDirectoryNotFound => "config directory not found", 84 | InvalidConfigDirectory => "invalid config directory", 85 | CannotCreateAppConfigDirectory => "cannot create the app config directory", 86 | AliasFileNotFound => "alias not found", 87 | AliasFilePermissionDenied => "permission denied", 88 | Unknown => "unknown alias error", 89 | } 90 | } 91 | } 92 | 93 | impl From for ErrorKind { 94 | fn from(err: IoErrorKind) -> ErrorKind { 95 | match err { 96 | IoErrorKind::NotFound => ErrorKind::AliasFileNotFound, 97 | IoErrorKind::PermissionDenied => ErrorKind::AliasFilePermissionDenied, 98 | _ => ErrorKind::Unknown, 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /crates/cli/src/commands/alias/help.rs: -------------------------------------------------------------------------------- 1 | // FIXME Duplicated code with HTTP command 2 | 3 | const LONG_FLAG_WIDTH: usize = 15; 4 | use crate::commands::alias::COMMAND_ALIAS; 5 | use crate::rh_name; 6 | 7 | macro_rules! newline { 8 | () => { 9 | println!("") 10 | }; 11 | } 12 | macro_rules! flags { 13 | ($description:expr, $long:expr) => { 14 | println!(" --{:long$} {}", $long, $description, long = LONG_FLAG_WIDTH) 15 | }; 16 | ($description:expr, $long:expr, $short:expr) => { 17 | println!(" -{}, --{:long$} {}", $short, $long, $description, long = LONG_FLAG_WIDTH) 18 | }; 19 | } 20 | macro_rules! key_value { 21 | ($description:expr, $long:expr) => { 22 | println!(" {:long$} {}", $long, $description, long = LONG_FLAG_WIDTH + 2) 23 | }; 24 | } 25 | macro_rules! text { 26 | ($description:expr) => { 27 | println!(" {:long$} {}", "", $description, long = 3) 28 | }; 29 | } 30 | macro_rules! right_text { 31 | ($description:expr) => { 32 | println!(" {:long$} {}", "", $description, long = LONG_FLAG_WIDTH + 6) 33 | }; 34 | } 35 | 36 | macro_rules! action { 37 | () => { 38 | println!("ACTION:"); 39 | flags!("Create a new alias (default action if no action is specified)", "add"); 40 | flags!("Delete an alias", "delete"); 41 | flags!("List all aliases", "list"); 42 | }; 43 | } 44 | macro_rules! alias { 45 | () => { 46 | println!("ALIAS:"); 47 | key_value!("An alias starts with a @", "@alias"); 48 | right_text!("If there is no alias then it will be the default alias"); 49 | }; 50 | } 51 | macro_rules! options { 52 | () => { 53 | println!("OPTIONS:"); 54 | text!(format!("Any options you can use with the {} command", rh_name!())); 55 | text!("There are no options for --delete and --list actions"); 56 | }; 57 | } 58 | 59 | macro_rules! thanks { 60 | () => { 61 | println!("Thanks for using {}!", rh_name!()) 62 | }; 63 | } 64 | 65 | pub fn show() { 66 | println!("USAGE:"); 67 | text!(format!("{} {} [action] [@alias] [options]", rh_name!(), COMMAND_ALIAS)); 68 | 69 | newline!(); 70 | action!(); 71 | newline!(); 72 | alias!(); 73 | newline!(); 74 | options!(); 75 | newline!(); 76 | thanks!(); 77 | } 78 | -------------------------------------------------------------------------------- /crates/cli/src/commands/alias/mod.rs: -------------------------------------------------------------------------------- 1 | mod action; 2 | mod error; 3 | mod help; 4 | pub(crate) mod storage; 5 | 6 | use super::{Command, DonePtr, ALIAS_NAME_PREFIX}; 7 | use crate::core::Args; 8 | use crate::shell::os::OsDirs; 9 | use crate::shell::Shell; 10 | use action::Action; 11 | pub(crate) use error::Error; 12 | use std::io::Write; 13 | pub use storage::load::from_default; 14 | pub use storage::load::from_name; 15 | use storage::DEFAULT_ALIAS_NAME; 16 | 17 | pub type Result = std::result::Result; 18 | 19 | pub const COMMAND_ALIAS: &str = "alias"; 20 | 21 | pub struct AliasCommand; 22 | 23 | // FIXME must use the shell abstraction instead of println!() 24 | impl Command for AliasCommand { 25 | fn execute(&self, shell: &mut Shell, args: &mut Args, _: DonePtr) -> super::Result<()> { 26 | remove_the_first_arg_that_is_the_alias_command(args); 27 | let subcommand = action::get(args, is_valid_alias, fix_alias_name)?; 28 | 29 | match subcommand { 30 | Action::Add => { 31 | let name = alias_name(args)?; 32 | storage::store::save(shell.os_dirs(), &name, args)?; 33 | println!("Alias '{}' saved", name); 34 | } 35 | Action::Delete => { 36 | let name = alias_name(args)?; 37 | storage::store::delete(shell.os_dirs(), &name)?; 38 | println!("Alias '{}' deleted", name); 39 | } 40 | Action::List => { 41 | let mut aliases = storage::show::list(shell.os_dirs())?; 42 | aliases.sort_unstable_by(|alias1, alias2| alias1.name.cmp(&alias2.name)); 43 | let alias_count = aliases.len(); 44 | if alias_count > 0 { 45 | println!("Found {} {}:", alias_count, if alias_count > 1 { "aliases" } else { "alias" }); 46 | for alias in aliases { 47 | println!("{}{:width$} {}", ALIAS_NAME_PREFIX, alias.name, alias.args.join(" "), width = 12); 48 | } 49 | } else { 50 | println!("No aliases found"); 51 | } 52 | } 53 | Action::Help => { 54 | help::show(); 55 | } 56 | }; 57 | 58 | Ok(()) 59 | } 60 | } 61 | 62 | fn remove_the_first_arg_that_is_the_alias_command(args: &mut Args) { 63 | if !args.is_empty() { 64 | args.remove(0); 65 | } 66 | } 67 | 68 | fn is_valid_alias(arg: Option<&String>) -> bool { 69 | match arg { 70 | Some(arg) => arg.starts_with(ALIAS_NAME_PREFIX) && arg.len() > 1, 71 | None => false, 72 | } 73 | } 74 | 75 | fn fix_alias_name(alias_name: &str) -> String { 76 | if let Some(alias_name) = alias_name.strip_prefix(ALIAS_NAME_PREFIX) { 77 | alias_name.to_string() 78 | } else { 79 | alias_name.to_string() 80 | } 81 | } 82 | 83 | fn alias_name(args: &mut Args) -> Result { 84 | if let Some(first) = args.first() { 85 | if let Some(alias_name) = first.strip_prefix(ALIAS_NAME_PREFIX) { 86 | let alias_name = alias_name.to_string(); 87 | args.remove(0); 88 | return Ok(alias_name.to_lowercase()); 89 | } 90 | } 91 | Ok(DEFAULT_ALIAS_NAME.to_string()) 92 | } 93 | 94 | // UNIT TESTS ///////////////////////////////////////////////////////////////////////////// 95 | 96 | #[cfg(test)] 97 | mod tests { 98 | use super::*; 99 | use crate::commands::alias::error::ErrorKind; 100 | use crate::commands::Command; 101 | use crate::core::{Error as CoreError, Result}; 102 | use crate::test::alias::*; 103 | use crate::test::os::{TestInvalidOsDirs, TestNoOsDirs, TestValidOsDirs}; 104 | 105 | mod basic { 106 | use super::*; 107 | 108 | #[test] 109 | fn fix_alias() { 110 | assert_eq!(fix_alias_name("hello"), "hello"); 111 | assert_eq!(fix_alias_name("@hello"), "hello"); 112 | assert_eq!(fix_alias_name(""), ""); 113 | assert_eq!(fix_alias_name("@"), ""); 114 | } 115 | 116 | #[test] 117 | fn valid_alias() { 118 | assert_eq!(is_valid_alias(Some(&"hello".to_string())), false); 119 | assert_eq!(is_valid_alias(Some(&"".to_string())), false); 120 | assert_eq!(is_valid_alias(Some(&ALIAS_NAME_PREFIX.to_string())), false); 121 | assert!(is_valid_alias(Some(&format!("{}hello", ALIAS_NAME_PREFIX)))); 122 | } 123 | } 124 | 125 | mod basic_errors { 126 | use super::*; 127 | 128 | #[test] 129 | fn error_if_no_args_for_default_alias() { 130 | setup(); 131 | 132 | let mut args = rh_test::args!["alias"]; 133 | let os_dirs = TestValidOsDirs::new(); 134 | let mut shell = Shell::new(&os_dirs, Vec::new(), Vec::new()); 135 | let command = AliasCommand {}; 136 | let res = command.execute(&mut shell, &mut args, || {}); 137 | assert!(res.is_err()); 138 | } 139 | 140 | #[test] 141 | fn error_if_no_args_for_custom_alias() { 142 | setup(); 143 | let mut args = rh_test::args!["alias", rh_test::arg_alias!(CUSTOM_ALIAS_NAME_2)]; 144 | let os_dirs = TestValidOsDirs::new(); 145 | let mut shell = Shell::new(&os_dirs, Vec::new(), Vec::new()); 146 | let command = AliasCommand {}; 147 | let res = command.execute(&mut shell, &mut args, || {}); 148 | assert!(res.is_err()); 149 | } 150 | } 151 | 152 | mod create { 153 | use super::*; 154 | 155 | fn test_alias_with(os_dirs: OD, alias_name: &str, mut args: Args) -> Result { 156 | setup(); 157 | assert_eq!(alias_exists(alias_name), false); 158 | let mut shell = Shell::new(&os_dirs, Vec::new(), Vec::new()); 159 | let command = AliasCommand {}; 160 | command.execute(&mut shell, &mut args, || {})?; 161 | Ok(args) 162 | } 163 | 164 | mod default { 165 | use super::*; 166 | 167 | fn test_default_alias_with(os_dirs: OD, url: &str) -> Result { 168 | test_alias_with(os_dirs, DEFAULT_ALIAS_NAME, rh_test::args!["alias", url.clone()]) 169 | } 170 | #[test] 171 | fn default_alias() { 172 | let args = test_default_alias_with(TestValidOsDirs::new(), "http://test.com/abc").expect("Cannot execute default_alias"); 173 | assert!(alias_exists(DEFAULT_ALIAS_NAME)); 174 | assert_eq!(args, vec!["http://test.com/abc"]); 175 | } 176 | #[test] 177 | fn error_if_no_config_directory_for_default_alias() { 178 | let res = test_default_alias_with(TestNoOsDirs::new(), "http://test.com/abc"); 179 | assert!(res.is_err()); 180 | assert_eq!( 181 | res.unwrap_err(), 182 | CoreError::AliasCommand(Error::CannotCreateAlias(DEFAULT_ALIAS_NAME.into(), ErrorKind::ConfigDirectoryNotFound)) 183 | ); 184 | } 185 | #[test] 186 | fn error_if_invalid_config_directory_for_default_alias() { 187 | let res = test_default_alias_with(TestInvalidOsDirs::new(), "http://test.com/abc"); 188 | assert!(res.is_err()); 189 | assert_eq!( 190 | res.unwrap_err(), 191 | CoreError::AliasCommand(Error::CannotCreateAlias(DEFAULT_ALIAS_NAME.into(), ErrorKind::InvalidConfigDirectory)) 192 | ); 193 | } 194 | } 195 | 196 | mod custom { 197 | use super::*; 198 | 199 | fn test_custom_alias(os_dirs: OD, url: &str) -> Result { 200 | test_alias_with(os_dirs, CUSTOM_ALIAS_NAME_1, rh_test::args!["alias", rh_test::arg_alias!(CUSTOM_ALIAS_NAME_1), url.clone()]) 201 | } 202 | 203 | #[test] 204 | fn custom_alias() { 205 | let args = test_custom_alias(TestValidOsDirs::new(), "http://test.com/def").expect("Cannot execute custom_alias"); 206 | assert!(alias_exists(CUSTOM_ALIAS_NAME_1)); 207 | assert_eq!(args, vec!["http://test.com/def"]); 208 | } 209 | #[test] 210 | fn error_if_no_config_directory_for_custom_alias() { 211 | let res = test_custom_alias(TestNoOsDirs::new(), "http://test.com/abc"); 212 | assert!(res.is_err()); 213 | assert_eq!( 214 | res.unwrap_err(), 215 | CoreError::AliasCommand(Error::CannotCreateAlias(CUSTOM_ALIAS_NAME_1.into(), ErrorKind::ConfigDirectoryNotFound)) 216 | ); 217 | } 218 | #[test] 219 | fn error_if_invalid_config_directory_for_custom_alias() { 220 | let res = test_custom_alias(TestInvalidOsDirs::new(), "http://test.com/abc"); 221 | assert!(res.is_err()); 222 | assert_eq!( 223 | res.unwrap_err(), 224 | CoreError::AliasCommand(Error::CannotCreateAlias(CUSTOM_ALIAS_NAME_1.into(), ErrorKind::InvalidConfigDirectory)) 225 | ); 226 | } 227 | } 228 | } 229 | 230 | mod only_single_flag { 231 | use super::*; 232 | 233 | fn delete_alias_with(os_dirs: OD, alias_name: &str, mut args: Args) -> Result { 234 | setup(); 235 | assert_eq!(alias_exists(alias_name), false); 236 | create_alias_file(alias_name); 237 | assert!(alias_exists(alias_name)); 238 | let mut shell = Shell::new(&os_dirs, Vec::new(), Vec::new()); 239 | let command = AliasCommand {}; 240 | command.execute(&mut shell, &mut args, || {})?; 241 | Ok(args) 242 | } 243 | 244 | mod list { 245 | use super::*; 246 | 247 | fn list_alias_with(os_dirs: OD) -> Result { 248 | delete_alias_with(os_dirs, DEFAULT_ALIAS_NAME, rh_test::args!["alias", "--list"]) 249 | } 250 | 251 | #[test] 252 | fn list() { 253 | list_alias_with(TestValidOsDirs::new()).expect("Cannot delete default_alias"); 254 | assert!(alias_exists(DEFAULT_ALIAS_NAME)); 255 | } 256 | #[test] 257 | fn error_if_no_config_directory_for_list() { 258 | let res = list_alias_with(TestNoOsDirs::new()); 259 | assert!(res.is_err()); 260 | assert_eq!(res.unwrap_err(), CoreError::AliasCommand(Error::CannotListAlias)); 261 | assert!(alias_exists(DEFAULT_ALIAS_NAME)); 262 | } 263 | #[test] 264 | fn error_if_invalid_config_directory_for_list() { 265 | let res = list_alias_with(TestInvalidOsDirs::new()); 266 | assert!(res.is_err()); 267 | assert_eq!(res.unwrap_err(), CoreError::AliasCommand(Error::CannotListAlias)); 268 | assert!(alias_exists(DEFAULT_ALIAS_NAME)); 269 | } 270 | } 271 | 272 | mod delete_default { 273 | use super::*; 274 | 275 | fn delete_default_alias_with(os_dirs: OD) -> Result { 276 | delete_alias_with(os_dirs, DEFAULT_ALIAS_NAME, rh_test::args!["alias", "--delete"]) 277 | } 278 | 279 | #[test] 280 | fn delete_default_alias() { 281 | delete_default_alias_with(TestValidOsDirs::new()).expect("Cannot delete default_alias"); 282 | assert_eq!(alias_exists(DEFAULT_ALIAS_NAME), false); 283 | } 284 | #[test] 285 | fn error_if_no_config_directory_for_delete_default_alias() { 286 | let res = delete_default_alias_with(TestNoOsDirs::new()); 287 | assert!(res.is_err()); 288 | assert_eq!( 289 | res.unwrap_err(), 290 | CoreError::AliasCommand(Error::CannotDeleteAlias(DEFAULT_ALIAS_NAME.into(), ErrorKind::ConfigDirectoryNotFound)) 291 | ); 292 | assert!(alias_exists(DEFAULT_ALIAS_NAME)); 293 | } 294 | #[test] 295 | fn error_if_invalid_config_directory_for_delete_default_alias() { 296 | let res = delete_default_alias_with(TestInvalidOsDirs::new()); 297 | assert!(res.is_err()); 298 | assert_eq!( 299 | res.unwrap_err(), 300 | CoreError::AliasCommand(Error::CannotDeleteAlias(DEFAULT_ALIAS_NAME.into(), ErrorKind::InvalidConfigDirectory)) 301 | ); 302 | assert!(alias_exists(DEFAULT_ALIAS_NAME)); 303 | } 304 | } 305 | 306 | mod delete_custom { 307 | use super::*; 308 | 309 | fn delete_custom_alias_with(os_dirs: OD) -> Result { 310 | delete_alias_with(os_dirs, CUSTOM_ALIAS_NAME_1, rh_test::args!["alias", "--delete", rh_test::arg_alias!(CUSTOM_ALIAS_NAME_1)]) 311 | } 312 | 313 | #[test] 314 | fn delete_custom_alias() { 315 | delete_custom_alias_with(TestValidOsDirs::new()).expect("Cannot delete custom_alias"); 316 | assert_eq!(alias_exists(CUSTOM_ALIAS_NAME_1), false); 317 | } 318 | #[test] 319 | fn error_if_no_config_directory_for_delete_custom_alias() { 320 | let res = delete_custom_alias_with(TestNoOsDirs::new()); 321 | assert!(res.is_err()); 322 | assert_eq!( 323 | res.unwrap_err(), 324 | CoreError::AliasCommand(Error::CannotDeleteAlias(CUSTOM_ALIAS_NAME_1.into(), ErrorKind::ConfigDirectoryNotFound)) 325 | ); 326 | assert!(alias_exists(CUSTOM_ALIAS_NAME_1)); 327 | } 328 | #[test] 329 | fn error_if_invalid_config_directory_for_delete_custom_alias() { 330 | let res = delete_custom_alias_with(TestInvalidOsDirs::new()); 331 | assert!(res.is_err()); 332 | assert_eq!( 333 | res.unwrap_err(), 334 | CoreError::AliasCommand(Error::CannotDeleteAlias(CUSTOM_ALIAS_NAME_1.into(), ErrorKind::InvalidConfigDirectory)) 335 | ); 336 | assert!(alias_exists(CUSTOM_ALIAS_NAME_1)); 337 | } 338 | } 339 | } 340 | 341 | mod error_delete_because_of_args { 342 | use super::*; 343 | 344 | #[test] 345 | fn error_delete_default_alias() { 346 | setup(); 347 | 348 | let mut args = rh_test::args!["alias", "--delete", "aBcD"]; 349 | let os_dirs = TestValidOsDirs::new(); 350 | let mut shell = Shell::new(&os_dirs, Vec::new(), Vec::new()); 351 | let command = AliasCommand {}; 352 | let res = command.execute(&mut shell, &mut args, || {}); 353 | assert!(res.is_err()); 354 | assert_eq!(res.unwrap_err(), CoreError::AliasCommand(Error::TooManyArgsForDelete("abcd".into()))); 355 | } 356 | 357 | #[test] 358 | fn error_delete_custom_alias() { 359 | setup(); 360 | 361 | let mut args = rh_test::args!["alias", "--delete", rh_test::arg_alias!(CUSTOM_ALIAS_NAME_1), "def"]; 362 | let os_dirs = TestValidOsDirs::new(); 363 | let mut shell = Shell::new(&os_dirs, Vec::new(), Vec::new()); 364 | let command = AliasCommand {}; 365 | let res = command.execute(&mut shell, &mut args, || {}); 366 | assert!(res.is_err()); 367 | assert_eq!(res.unwrap_err(), CoreError::AliasCommand(Error::TooManyArgsForDelete(CUSTOM_ALIAS_NAME_1.into()))); 368 | } 369 | } 370 | } 371 | -------------------------------------------------------------------------------- /crates/cli/src/commands/alias/storage/iter.rs: -------------------------------------------------------------------------------- 1 | use std::result::Result; 2 | 3 | #[derive(Clone)] 4 | pub struct FilterOkIterator { 5 | iter: I, 6 | predicate: P, 7 | } 8 | 9 | impl Iterator for FilterOkIterator 10 | where 11 | P: FnMut(&A) -> bool, 12 | I: Iterator>, 13 | { 14 | type Item = Result; 15 | 16 | #[inline] 17 | fn next(&mut self) -> Option> { 18 | for x in self.iter.by_ref() { 19 | match x { 20 | Ok(xx) => { 21 | if (self.predicate)(&xx) { 22 | return Some(Ok(xx)); 23 | } 24 | } 25 | Err(_) => return Some(x), 26 | } 27 | } 28 | None 29 | } 30 | } 31 | 32 | pub trait FilterOkTrait { 33 | fn filter_ok(self, predicate: P) -> FilterOkIterator 34 | where 35 | Self: Sized + Iterator>, 36 | P: FnMut(&A) -> bool, 37 | { 38 | FilterOkIterator { iter: self, predicate } 39 | } 40 | } 41 | 42 | impl FilterOkTrait for I where I: Sized + Iterator> {} 43 | -------------------------------------------------------------------------------- /crates/cli/src/commands/alias/storage/load.rs: -------------------------------------------------------------------------------- 1 | use super::alias_filename; 2 | use super::{iter::FilterOkTrait, DEFAULT_ALIAS_NAME}; 3 | use crate::commands::alias::{Error, Result}; 4 | use crate::core::Args; 5 | use crate::shell::os::OsDirs; 6 | use std::fs::File; 7 | use std::io::{self, BufRead, BufReader}; 8 | use std::path::Path; 9 | 10 | pub fn from_default(os_dirs: &OD) -> Result { 11 | from_name(os_dirs, DEFAULT_ALIAS_NAME) 12 | } 13 | 14 | pub fn from_name(os_dirs: &OD, name: &str) -> Result { 15 | match os_dirs.app_path(&alias_filename(name)) { 16 | Some(path) => match from_path(&path) { 17 | Ok(args) => Ok(args), 18 | Err(err) => Err(Error::CannotLoadAlias(name.into(), err.kind().into())), 19 | }, 20 | None => Ok(Args::new()), 21 | } 22 | } 23 | 24 | pub fn from_path(path: &Path) -> std::result::Result { 25 | match File::open(&path) { 26 | Ok(file) => from_reader(&file), 27 | Err(err) => Err(err), 28 | } 29 | } 30 | 31 | fn from_reader(reader: R) -> std::result::Result { 32 | let buffer = BufReader::new(reader); 33 | match buffer.lines().filter_ok(|arg| !arg.is_empty()).collect() { 34 | Ok(args) => Ok(args), 35 | Err(err) => Err(err), 36 | } 37 | } 38 | 39 | // UNIT TESTS ///////////////////////////////////////////////////////////////////////////// 40 | 41 | #[cfg(test)] 42 | mod tests { 43 | use super::*; 44 | use crate::{ 45 | commands::alias::error::ErrorKind, 46 | test::{alias::*, os::TestValidOsDirs}, 47 | }; 48 | 49 | mod basic { 50 | use super::*; 51 | 52 | #[test] 53 | fn lines() { 54 | let args = from_reader(&b"-cushH\nX-KEY-1:val1\nX-KEY-2:val2"[..]).unwrap(); 55 | assert_eq!(args, vec!["-cushH", "X-KEY-1:val1", "X-KEY-2:val2"]); 56 | } 57 | 58 | #[test] 59 | fn empty_lines() { 60 | let args = from_reader(&b"-cushH\n\n\nX-KEY-1:val1\nX-KEY-2:val2\n\n\n"[..]).unwrap(); 61 | assert_eq!(args, vec!["-cushH", "X-KEY-1:val1", "X-KEY-2:val2"]); 62 | } 63 | 64 | #[test] 65 | fn error_if_invalid_characters() { 66 | let res = from_reader(&b"\xAA-cushH\nX-KEY-1:val1\nX-KEY-2:val2"[..]); 67 | assert!(res.is_err()); 68 | // assert_eq!(res.unwrap_err(), Error::Config("testlines".into(), "testlines".into())); 69 | } 70 | } 71 | 72 | mod default_alias { 73 | use super::*; 74 | 75 | #[test] 76 | fn lines_from_default_alias() { 77 | setup(); 78 | assert_eq!(alias_exists(DEFAULT_ALIAS_NAME), false); 79 | create_alias_file(DEFAULT_ALIAS_NAME); 80 | assert!(alias_exists(DEFAULT_ALIAS_NAME)); 81 | 82 | let args = from_default(&TestValidOsDirs::new()).unwrap(); 83 | assert_eq!(args, vec!["-v", "-c"]); 84 | } 85 | 86 | #[test] 87 | fn error_if_default_alias_missing() { 88 | setup(); 89 | 90 | let res = from_default(&TestValidOsDirs::new()); 91 | assert!(res.is_err()); 92 | assert_eq!(res.unwrap_err(), Error::CannotLoadAlias(DEFAULT_ALIAS_NAME.into(), ErrorKind::AliasFileNotFound)); 93 | } 94 | } 95 | 96 | mod custom_alias { 97 | use super::*; 98 | 99 | #[test] 100 | fn lines_from_empty_alias() { 101 | setup(); 102 | assert_eq!(alias_exists(EMPTY_ALIAS_NAME), false); 103 | create_empty_alias_file(EMPTY_ALIAS_NAME); 104 | assert!(alias_exists(EMPTY_ALIAS_NAME)); 105 | 106 | let args = from_name(&TestValidOsDirs::new(), EMPTY_ALIAS_NAME).unwrap(); 107 | assert_eq!(args, Vec::::new()); 108 | } 109 | 110 | #[test] 111 | fn lines_from_1arg_alias() { 112 | setup(); 113 | assert_eq!(alias_exists(CUSTOM_ALIAS_NAME_1), false); 114 | create_alias_file_with_args(CUSTOM_ALIAS_NAME_1, "-cUs"); 115 | assert!(alias_exists(CUSTOM_ALIAS_NAME_1)); 116 | 117 | let args = from_name(&TestValidOsDirs::new(), CUSTOM_ALIAS_NAME_1).unwrap(); 118 | assert_eq!(args, vec!["-cUs"]); 119 | } 120 | 121 | #[test] 122 | fn lines_from_2args_alias() { 123 | setup(); 124 | assert_eq!(alias_exists(CUSTOM_ALIAS_NAME_2), false); 125 | create_alias_file_with_args(CUSTOM_ALIAS_NAME_2, "-UhH\nX-Key:Val"); 126 | assert!(alias_exists(CUSTOM_ALIAS_NAME_2)); 127 | 128 | let args = from_name(&TestValidOsDirs::new(), CUSTOM_ALIAS_NAME_2).unwrap(); 129 | assert_eq!(args, vec!["-UhH", "X-Key:Val"]); 130 | } 131 | 132 | #[test] 133 | fn error_if_no_alias_file() { 134 | setup(); 135 | let res = from_name(&TestValidOsDirs::new(), "non-existing-alias"); 136 | assert!(res.is_err()); 137 | assert_eq!(res.unwrap_err(), Error::CannotLoadAlias("non-existing-alias".into(), ErrorKind::AliasFileNotFound)); 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /crates/cli/src/commands/alias/storage/mod.rs: -------------------------------------------------------------------------------- 1 | mod iter; 2 | pub(crate) mod load; 3 | pub(crate) mod show; 4 | pub(crate) mod store; 5 | 6 | use crate::core::Result; 7 | pub use load::from_default; 8 | pub use load::from_name; 9 | 10 | pub const DEFAULT_ALIAS_NAME: &str = "default"; 11 | 12 | pub const ALIAS_FILENAME_PREFIX: &str = ".rh_"; 13 | pub const ALIAS_FILENAME_SUFFIX: &str = "_rc"; 14 | 15 | pub trait AliasArgItem { 16 | fn enrich_with_alias(&mut self) -> Result<()>; 17 | } 18 | 19 | fn alias_filename(name: &str) -> String { 20 | format!("{}{}{}", ALIAS_FILENAME_PREFIX, name, ALIAS_FILENAME_SUFFIX) 21 | } 22 | -------------------------------------------------------------------------------- /crates/cli/src/commands/alias/storage/show.rs: -------------------------------------------------------------------------------- 1 | use super::load::from_path; 2 | use super::{ALIAS_FILENAME_PREFIX, ALIAS_FILENAME_SUFFIX}; 3 | use crate::commands::alias::{Error, Result}; 4 | use crate::core::Args; 5 | use crate::shell::os::OsDirs; 6 | use std::fs::{self, ReadDir}; 7 | use std::path::Path; 8 | 9 | pub fn list(os_dirs: &OD) -> Result> { 10 | match os_dirs.app_config_directory() { 11 | Some(path) => Ok(read_app_config(&path)? 12 | .filter(|res| res.is_ok()) 13 | .map(|res| res.unwrap().path()) 14 | .filter(|path| path.is_file() && is_alias_file(path)) 15 | .map(|alias_path| alias(&alias_path)) 16 | .collect()), 17 | None => Err(Error::CannotListAlias), 18 | } 19 | } 20 | 21 | fn read_app_config(path: &Path) -> Result { 22 | match fs::read_dir(path) { 23 | Ok(res) => Ok(res), 24 | Err(_) => Err(Error::CannotListAlias), 25 | } 26 | } 27 | 28 | fn is_alias_file(path: &Path) -> bool { 29 | match path.file_name() { 30 | Some(filename) => { 31 | let filename = filename.to_str().unwrap(); 32 | filename.starts_with(ALIAS_FILENAME_PREFIX) && filename.ends_with(ALIAS_FILENAME_SUFFIX) 33 | } 34 | None => false, 35 | } 36 | } 37 | 38 | fn alias(path: &Path) -> Alias { 39 | let filename = path.file_name().unwrap().to_string_lossy(); 40 | let alias_start_pos_in_filename = ALIAS_FILENAME_PREFIX.len(); 41 | let alias_end_pos_in_filename = filename.len() - ALIAS_FILENAME_SUFFIX.len(); 42 | 43 | let args = match from_path(path) { 44 | Ok(args) => args, 45 | Err(_) => vec!["Can't load arguments".to_string()], 46 | }; 47 | 48 | Alias { 49 | name: filename[alias_start_pos_in_filename..alias_end_pos_in_filename].to_string(), 50 | args, 51 | } 52 | } 53 | 54 | pub struct Alias { 55 | pub name: String, 56 | pub args: Args, 57 | } 58 | -------------------------------------------------------------------------------- /crates/cli/src/commands/alias/storage/store.rs: -------------------------------------------------------------------------------- 1 | use super::alias_filename; 2 | use crate::commands::alias::error::ErrorKind; 3 | use crate::commands::alias::Error; 4 | use crate::commands::alias::Result; 5 | use crate::core::Args; 6 | use crate::shell::os::OsDirs; 7 | use std::fs::{self, File}; 8 | use std::io::{self, BufWriter, Write}; 9 | use std::path::Path; 10 | 11 | pub fn save(os_dirs: &OD, name: &str, args: &Args) -> Result<()> { 12 | match os_dirs.app_path(&alias_filename(name)) { 13 | Some(path) => match get_config_directory_error_if_any(os_dirs) { 14 | None => { 15 | let file = create_alias_file(name, &path)?; 16 | write(args, &file) 17 | } 18 | Some(err_kind) => Err(Error::CannotCreateAlias(name.to_string(), err_kind)), 19 | }, 20 | None => Err(Error::CannotCreateAlias(name.to_string(), ErrorKind::ConfigDirectoryNotFound)), 21 | } 22 | } 23 | 24 | pub fn delete(os_dirs: &OD, name: &str) -> Result<()> { 25 | match os_dirs.app_path(&alias_filename(name)) { 26 | Some(path) => match get_config_directory_error_if_any(os_dirs) { 27 | None => match fs::remove_file(&path) { 28 | Ok(_) => Ok(()), 29 | Err(err) => Err(Error::CannotDeleteAlias(name.to_string(), err.kind().into())), 30 | }, 31 | Some(err_kind) => Err(Error::CannotDeleteAlias(name.to_string(), err_kind)), 32 | }, 33 | None => Err(Error::CannotDeleteAlias(name.to_string(), ErrorKind::ConfigDirectoryNotFound)), 34 | } 35 | } 36 | 37 | fn write(args: &Args, writer: R) -> Result<()> { 38 | let mut buffer = BufWriter::new(writer); 39 | buffer.write_all(args.join("\n").as_bytes()).expect("Unable to write data"); 40 | Ok(()) 41 | } 42 | 43 | fn get_config_directory_error_if_any(os_dirs: &OD) -> Option { 44 | match os_dirs.config_directory() { 45 | Some(dir) => { 46 | if Path::exists(&dir) { 47 | None 48 | } else { 49 | Some(ErrorKind::InvalidConfigDirectory) 50 | } 51 | } 52 | None => Some(ErrorKind::ConfigDirectoryNotFound), 53 | } 54 | } 55 | 56 | fn create_alias_file(name: &str, path: &Path) -> Result { 57 | match path.parent() { 58 | Some(dir) => { 59 | if !Path::exists(dir) && fs::create_dir(dir).is_err() { 60 | return Err(Error::CannotCreateAlias(name.to_string(), ErrorKind::CannotCreateAppConfigDirectory)); 61 | } 62 | let file = File::create(&path)?; 63 | Ok(file) 64 | } 65 | None => Err(Error::CannotCreateAlias(name.to_string(), ErrorKind::CannotCreateAppConfigDirectory)), 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /crates/cli/src/commands/args.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "alias")] 2 | use super::{ 3 | alias::{from_default, from_name, AliasCommand, COMMAND_ALIAS}, 4 | ALIAS_NAME_PREFIX, 5 | }; 6 | use super::{http::HttpCommand, ArgsCommand, Command, Result}; 7 | use crate::{ 8 | core::{Args, Error}, 9 | shell::os::OsDirs, 10 | }; 11 | use std::io::Write; 12 | 13 | impl ArgsCommand for Args { 14 | fn command(&mut self, os_dirs: &OD) -> Result>> { 15 | #[cfg(feature = "alias")] 16 | match self.first() { 17 | Some(first) => { 18 | if first == COMMAND_ALIAS { 19 | Ok(Box::new(AliasCommand {})) 20 | } else if let Some(alias_name) = first.strip_prefix(ALIAS_NAME_PREFIX) { 21 | match from_name(os_dirs, alias_name) { 22 | Ok(mut config_args) => { 23 | self.splice(..1, config_args.drain(..)); 24 | Ok(Box::new(HttpCommand {})) 25 | } 26 | Err(super::alias::Error::CannotLoadAlias(alias_name, _kind)) => Err(Error::Alias(format!("{}{}", ALIAS_NAME_PREFIX, alias_name))), 27 | Err(_) => Err(Error::AliasOther), // FIXME Not good 28 | } 29 | } else { 30 | if let Ok(mut config_args) = from_default(os_dirs) { 31 | self.splice(..0, config_args.drain(..)); 32 | } 33 | Ok(Box::new(HttpCommand {})) 34 | } 35 | } 36 | None => Err(Error::NoArgs), 37 | } 38 | #[cfg(not(feature = "alias"))] 39 | { 40 | if !self.is_empty() { 41 | Ok(Box::new(HttpCommand {})) 42 | } else { 43 | Err(Error::NoArgs) 44 | } 45 | } 46 | } 47 | } 48 | 49 | #[cfg(test)] 50 | mod tests { 51 | use super::*; 52 | use crate::commands::alias::storage::DEFAULT_ALIAS_NAME; 53 | use crate::test::alias::*; 54 | use crate::test::os::TestValidOsDirs; 55 | 56 | #[test] 57 | fn default_alias() { 58 | setup(); 59 | assert_eq!(alias_exists(DEFAULT_ALIAS_NAME), false); 60 | create_alias_file(DEFAULT_ALIAS_NAME); 61 | assert!(alias_exists(DEFAULT_ALIAS_NAME)); 62 | 63 | let mut args = rh_test::args!["-cuh", "http://test.com"]; 64 | let _: Box, &mut Vec>> = args.command(&TestValidOsDirs::new()).expect("Cannot execute default_alias"); 65 | assert_eq!(args, vec!["-v", "-c", "-cuh", "http://test.com"]); 66 | } 67 | 68 | #[test] 69 | fn empty_alias() { 70 | setup(); 71 | assert_eq!(alias_exists(EMPTY_ALIAS_NAME), false); 72 | create_empty_alias_file(EMPTY_ALIAS_NAME); 73 | assert!(alias_exists(EMPTY_ALIAS_NAME)); 74 | 75 | let mut args = rh_test::args![rh_test::arg_alias!(EMPTY_ALIAS_NAME), "-cuh", "http://test.com"]; 76 | let _: Box, &mut Vec>> = args.command(&TestValidOsDirs::new()).expect("Cannot execute empty_alias"); 77 | assert_eq!(args, vec!["-cuh", "http://test.com"]); 78 | } 79 | 80 | #[test] 81 | fn custom_alias_with_one_arg() { 82 | setup(); 83 | assert_eq!(alias_exists(CUSTOM_ALIAS_NAME_1), false); 84 | create_alias_file_with_args(CUSTOM_ALIAS_NAME_1, "-cUs"); 85 | assert!(alias_exists(CUSTOM_ALIAS_NAME_1)); 86 | 87 | let mut args = rh_test::args![rh_test::arg_alias!(CUSTOM_ALIAS_NAME_1), "-cUh", "http://test.com"]; 88 | let _: Box, &mut Vec>> = args.command(&TestValidOsDirs::new()).unwrap(); 89 | assert_eq!(args, vec!["-cUs", "-cUh", "http://test.com"]); 90 | } 91 | 92 | #[test] 93 | fn custom_alias_with_multi_args() { 94 | setup(); 95 | assert_eq!(alias_exists(CUSTOM_ALIAS_NAME_2), false); 96 | create_alias_file_with_args(CUSTOM_ALIAS_NAME_2, "-UhH\nX-Key:Val"); 97 | assert!(alias_exists(CUSTOM_ALIAS_NAME_2)); 98 | 99 | let mut args = rh_test::args![rh_test::arg_alias!(CUSTOM_ALIAS_NAME_2), "-cUh", "http://test.com"]; 100 | let _: Box, &mut Vec>> = args.command(&TestValidOsDirs::new()).unwrap(); 101 | assert_eq!(args, vec!["-UhH", "X-Key:Val", "-cUh", "http://test.com"]); 102 | } 103 | 104 | #[test] 105 | fn error_config() { 106 | let mut args = rh_test::args![rh_test::arg_alias!("error"), "-cuh", "http://test.com"]; 107 | let res: Result, &mut Vec>>> = args.command(&TestValidOsDirs::new()); 108 | assert!(res.is_err()); 109 | // FIXME Checks the error 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /crates/cli/src/commands/debug.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "alias")] 2 | use crate::shell::os::OsDirs; 3 | use std::env; 4 | #[cfg(feature = "alias")] 5 | use std::path::Path; 6 | 7 | const KEY_WIDTH: usize = 25; 8 | 9 | pub fn show() { 10 | show_program(); 11 | #[cfg(feature = "alias")] 12 | { 13 | println!(); 14 | show_directories(); 15 | } 16 | println!(); 17 | show_env_vars(); 18 | } 19 | 20 | fn show_program() { 21 | println!("{:width$} {}", "Name", crate::rh_name!(), width = KEY_WIDTH); 22 | println!("{:width$} {}", "Version", crate::rh_version!(), width = KEY_WIDTH); 23 | println!("{:width$} {}", "Homepage", crate::rh_homepage!(), width = KEY_WIDTH); 24 | } 25 | 26 | #[cfg(feature = "alias")] 27 | fn show_directories() { 28 | use crate::shell::os::DefaultOsDirs; 29 | 30 | let os_dirs = DefaultOsDirs::default(); 31 | let mut config_dir_exists = false; 32 | match os_dirs.config_directory() { 33 | Some(path) => { 34 | println!("{:width$} {}", "Config location", path.display(), width = KEY_WIDTH); 35 | config_dir_exists = Path::new(&path).exists(); 36 | if !config_dir_exists { 37 | println!("{:width$} Cannot find this directory on your platform", "", width = KEY_WIDTH); 38 | } 39 | } 40 | None => { 41 | println!("{:width$} cannot find the config path on your platform", "Config location", width = KEY_WIDTH); 42 | } 43 | }; 44 | 45 | if config_dir_exists { 46 | let path = match os_dirs.app_config_directory() { 47 | Some(path) => path.display().to_string(), 48 | None => String::from("cannot find the alias path on your platform"), 49 | }; 50 | println!("{:width$} {}", "Aliases location", path, width = KEY_WIDTH); 51 | } else { 52 | println!("{:width$} alias feature disabled on your platform", "Aliases location", width = KEY_WIDTH); 53 | } 54 | } 55 | 56 | fn show_env_vars() { 57 | env::vars().into_iter().for_each(|(name, value)| println!("{:width$} {}", name, value, width = KEY_WIDTH)); 58 | } 59 | -------------------------------------------------------------------------------- /crates/cli/src/commands/http/help.rs: -------------------------------------------------------------------------------- 1 | // FIXME Duplicated code with ALIAS command 2 | 3 | const LONG_FLAG_WIDTH: usize = 15; 4 | #[cfg(feature = "alias")] 5 | use crate::commands::alias::COMMAND_ALIAS; 6 | use crate::rh_name; 7 | 8 | macro_rules! newline { 9 | () => { 10 | println!("") 11 | }; 12 | } 13 | 14 | macro_rules! logo { 15 | () => { 16 | println!( 17 | "╱╱╭╮ 18 | ╱╱┃┃ 19 | ╭━┫╰━╮ 20 | ┃╭┫╭╮┃ 21 | ┃┃┃┃┃┃ 22 | ╰╯╰╯╰╯" 23 | ) 24 | }; 25 | } 26 | 27 | macro_rules! flags { 28 | ($description:expr, $long:expr) => { 29 | println!(" --{:long$} {}", $long, $description, long = LONG_FLAG_WIDTH) 30 | }; 31 | ($description:expr, $long:expr, $short:expr) => { 32 | println!(" -{}, --{:long$} {}", $short, $long, $description, long = LONG_FLAG_WIDTH) 33 | }; 34 | } 35 | macro_rules! key_value { 36 | ($description:expr, $long:expr) => { 37 | println!(" {:long$} {}", $long, $description, long = LONG_FLAG_WIDTH + 2) 38 | }; 39 | } 40 | macro_rules! text { 41 | ($description:expr) => { 42 | println!(" {:long$} {}", "", $description, long = 3) 43 | }; 44 | } 45 | macro_rules! right_text { 46 | ($description:expr) => { 47 | println!(" {:long$} {}", "", $description, long = LONG_FLAG_WIDTH + 6) 48 | }; 49 | } 50 | 51 | #[cfg(feature = "alias")] 52 | macro_rules! try_help_alias { 53 | () => { 54 | right_text!(format!("try '{} {} --help' for more information", rh_name!(), COMMAND_ALIAS)); 55 | }; 56 | } 57 | 58 | #[cfg(feature = "alias")] 59 | macro_rules! alias { 60 | () => { 61 | println!("ALIAS:"); 62 | key_value!("An alias starts with a @", "@alias"); 63 | try_help_alias!(); 64 | }; 65 | } 66 | macro_rules! method { 67 | () => { 68 | println!("METHOD:"); 69 | key_value!("If there is no data items then GET is the default method", "GET"); 70 | key_value!("If there are data items then POST is the default method", "POST"); 71 | key_value!("You can force any standard method (upper case)", "Standard method"); 72 | right_text!("GET|POST|PUT|DELETE|HEAD|OPTIONS|CONNECT|PATCH|TRACE"); 73 | key_value!("You can use a custom method (upper case)", "Custom method"); 74 | }; 75 | } 76 | macro_rules! options { 77 | () => { 78 | println!("OPTIONS:"); 79 | flags!("Show version", "version"); 80 | flags!("Show this screen", "help"); 81 | flags!("Show a symbol for the request part and another one for the response part", "direction", "d"); 82 | flags!("Colorize the output (shortcut: --pretty=c)", "pretty=color"); 83 | flags!("Show more details, shortcut for -UHBshb", "verbose", "v"); 84 | flags!("Show the request and response headers", "headers"); 85 | flags!("Show the request URL and method", "url", "U"); 86 | flags!("Show the request header", "req-header", "H"); 87 | flags!("Show the request payload", "req-body", "B"); 88 | flags!("Compact the request payload", "req-compact", "C"); 89 | flags!("Show the response status and HTTP version", "status", "s"); 90 | flags!("Show the response header", "header", "h"); 91 | flags!("Show the response body (default)", "body", "b"); 92 | flags!("Hide the response body", "body=n"); 93 | flags!("Compact the response body", "compact", "c"); 94 | newline!(); 95 | key_value!("Combine any short flags, for example:", "-cUh..."); 96 | right_text!("-c compact the response"); 97 | right_text!("-U url and method"); 98 | right_text!("-h response header"); 99 | }; 100 | } 101 | macro_rules! headers { 102 | () => { 103 | println!("HEADERS:"); 104 | key_value!("List of key:value space-separated", "..."); 105 | }; 106 | } 107 | macro_rules! body { 108 | () => { 109 | println!("PAYLOAD:"); 110 | flags!("Set the payload and don't apply any transformation", "raw="); 111 | flags!("Force the 'Accept' header to 'application/json' (default)", "json"); 112 | flags!("Set the 'Content-Type' and serialize data items as form URL encoded", "form"); 113 | key_value!("Data items as a list of key=value space-separated", "..."); 114 | right_text!("Data items are converted to JSON (default) or URL encoded (--form)"); 115 | }; 116 | } 117 | 118 | #[cfg(feature = "alias")] 119 | macro_rules! subcommand { 120 | () => { 121 | println!("SUBCOMMAND:"); 122 | key_value!("Manage aliases", COMMAND_ALIAS); 123 | try_help_alias!(); 124 | }; 125 | } 126 | 127 | macro_rules! thanks { 128 | () => { 129 | println!("Thanks for using {}!", rh_name!()) 130 | }; 131 | } 132 | 133 | pub fn show() { 134 | logo!(); 135 | 136 | newline!(); 137 | println!("USAGE:"); 138 | #[cfg(feature = "alias")] 139 | { 140 | text!(format!("{} [@alias] [METHOD] url [options] [headers] [payload]", rh_name!())); 141 | } 142 | #[cfg(not(feature = "alias"))] 143 | { 144 | text!(format!("{} [METHOD] url [options] [headers] [payload]", rh_name!())); 145 | } 146 | text!(format!("{} --help | -h", rh_name!())); 147 | text!(format!("{} --version", rh_name!())); 148 | #[cfg(feature = "alias")] 149 | { 150 | newline!(); 151 | text!(format!("{} [SUBCOMMAND] [options]", rh_name!())); 152 | text!(format!("{} [SUBCOMMAND] --help | -h", rh_name!())); 153 | } 154 | 155 | #[cfg(feature = "alias")] 156 | { 157 | newline!(); 158 | alias!(); 159 | } 160 | newline!(); 161 | method!(); 162 | newline!(); 163 | options!(); 164 | newline!(); 165 | headers!(); 166 | newline!(); 167 | body!(); 168 | newline!(); 169 | #[cfg(feature = "alias")] 170 | { 171 | subcommand!(); 172 | newline!(); 173 | } 174 | thanks!(); 175 | } 176 | -------------------------------------------------------------------------------- /crates/cli/src/commands/http/mod.rs: -------------------------------------------------------------------------------- 1 | mod help; 2 | mod output; 3 | mod render; 4 | mod version; 5 | 6 | use super::debug; 7 | use super::{Command, DonePtr, Result}; 8 | use crate::core::Args; 9 | use crate::core::Mode; 10 | use crate::parser; 11 | use crate::request; 12 | use crate::shell::os::OsDirs; 13 | use crate::shell::Shell; 14 | use std::io::Write; 15 | 16 | pub struct HttpCommand; 17 | 18 | impl Command for HttpCommand { 19 | fn execute(&self, shell: &mut Shell, args: &mut Args, _: DonePtr) -> Result<()> { 20 | let ws = parser::execute(args)?; 21 | match ws.mode() { 22 | Mode::Help => help::show(), 23 | Mode::Version => version::show(), 24 | Mode::Debug => debug::show(), 25 | Mode::Run => { 26 | { 27 | let mut headers = ws.headers.borrow_mut(); 28 | request::headers::upgrade(&ws, &mut headers); 29 | } 30 | let headers = ws.headers.borrow(); 31 | let req_number = 0u8; 32 | let response = request::execute(&ws, req_number, &headers)?; 33 | output::render(shell, &ws, req_number, response)?; 34 | } 35 | } 36 | Ok(()) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /crates/cli/src/commands/http/output.rs: -------------------------------------------------------------------------------- 1 | use super::render::RequestRender; 2 | use super::render::ResponseRender; 3 | use crate::core::Result; 4 | use crate::core::Workspace; 5 | use crate::request::Response; 6 | use crate::shell::os::OsDirs; 7 | use crate::shell::Shell; 8 | use std::borrow::Borrow; 9 | use std::cell::RefCell; 10 | use std::io; 11 | use std::io::Read; 12 | use std::io::Write; 13 | 14 | pub fn render(shell: &mut Shell, ws: &Workspace, _req_number: u8, response: Response) -> Result<()> { 15 | if ws.output_redirected && !ws.flags.borrow().use_color { 16 | render_raw_content(ws, RefCell::new(response))?; 17 | } else { 18 | let style_enabled = shell.enable_colors(); 19 | 20 | let headers = ws.headers.borrow(); 21 | let rf = RequestRender::new(ws, &headers, ws.theme.as_ref(), style_enabled); 22 | shell.out(rf)?; 23 | 24 | let rf = ResponseRender::new(ws, RefCell::new(response), ws.theme.as_ref(), style_enabled); 25 | shell.out(rf)?; 26 | } 27 | Ok(()) 28 | } 29 | 30 | fn render_raw_content(_args: &Workspace, response: RefCell) -> io::Result<()> { 31 | let mut bytes = Vec::new(); 32 | let mut response = response.borrow_mut(); 33 | response.read_to_end(&mut bytes)?; 34 | io::stdout().write_all(&bytes) 35 | } 36 | -------------------------------------------------------------------------------- /crates/cli/src/commands/http/render/header.rs: -------------------------------------------------------------------------------- 1 | use super::{HeaderRender, Render}; 2 | use crate::request::header::StandardHeader; 3 | use crate::request::HeaderMap; 4 | use crate::theme::HeaderTheme; 5 | use crate::{core::Workspace, theme::DirectionTheme}; 6 | use std::io::{Result, Write}; 7 | 8 | impl<'a> HeaderRender<'a> { 9 | pub fn new( 10 | workspace: &'a Workspace, 11 | headers: &'a HeaderMap, 12 | header_theme: &'a dyn HeaderTheme, 13 | direction_theme: &'a dyn DirectionTheme, 14 | direction_symbol: &'a [u8], 15 | style_enabled: bool, 16 | ) -> Self { 17 | Self { 18 | workspace, 19 | headers, 20 | header_theme, 21 | direction_theme, 22 | direction_symbol, 23 | style_enabled, 24 | } 25 | } 26 | } 27 | 28 | impl<'a> Render for HeaderRender<'a> { 29 | #[inline] 30 | fn write(&self, writer: &mut W) -> Result<()> 31 | where 32 | W: Write, 33 | { 34 | let flags = self.workspace.flags; 35 | let header_theme = self.header_theme; 36 | 37 | for (key, value) in self.headers.iter() { 38 | let is_standard = key.is_standard(); 39 | let key_style = header_theme.header_name(is_standard); 40 | let key = key.as_str(); 41 | 42 | if flags.show_direction { 43 | self.write_direction(writer, is_standard)?; 44 | } 45 | self.write_with_style(writer, key.as_bytes(), &key_style)?; 46 | self.write_with_style(writer, ": ".as_bytes(), &key_style)?; 47 | self.write_with_style(writer, value.to_str().unwrap_or("No value").as_bytes(), &header_theme.header_value(is_standard))?; 48 | self.write_newline(writer)?; 49 | } 50 | Ok(()) 51 | } 52 | 53 | #[inline] 54 | fn is_style_active(&self) -> bool { 55 | self.style_enabled 56 | } 57 | } 58 | 59 | impl<'a> HeaderRender<'a> { 60 | #[inline] 61 | fn write_direction(&self, writer: &mut W, is_standard: bool) -> Result<()> { 62 | self.write_with_style(writer, self.direction_symbol, &self.direction_theme.direction(is_standard)) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /crates/cli/src/commands/http/render/mod.rs: -------------------------------------------------------------------------------- 1 | mod header; 2 | mod request; 3 | mod response; 4 | 5 | use crate::core::Workspace; 6 | use crate::request::{HeaderMap, Response}; 7 | use crate::shell::Render; 8 | use crate::theme::{DirectionTheme, HeaderTheme, Theme}; 9 | use std::cell::RefCell; 10 | 11 | pub const DIRECTION_REQUEST: &[u8] = b"> "; 12 | pub const DIRECTION_RESPONSE: &[u8] = b"< "; 13 | 14 | pub struct RequestRender<'a> { 15 | workspace: &'a Workspace, 16 | headers: &'a HeaderMap, 17 | theme: &'a dyn Theme, 18 | style_enabled: bool, 19 | req_number: usize, 20 | } 21 | 22 | pub struct ResponseRender<'a> { 23 | workspace: &'a Workspace, 24 | response: RefCell, 25 | theme: &'a dyn Theme, 26 | style_enabled: bool, 27 | } 28 | 29 | pub struct HeaderRender<'a> { 30 | workspace: &'a Workspace, 31 | headers: &'a HeaderMap, 32 | header_theme: &'a dyn HeaderTheme, 33 | direction_theme: &'a dyn DirectionTheme, 34 | direction_symbol: &'a [u8], 35 | style_enabled: bool, 36 | } 37 | -------------------------------------------------------------------------------- /crates/cli/src/commands/http/render/request.rs: -------------------------------------------------------------------------------- 1 | use super::{HeaderRender, Render, RequestRender, DIRECTION_REQUEST}; 2 | use crate::items::Items; 3 | use crate::request::HeaderMap; 4 | use crate::shell::form::FormRender; 5 | use crate::shell::json::JsonRender; 6 | use crate::{ 7 | core::{Workspace, WorkspaceData}, 8 | theme::Theme, 9 | }; 10 | use std::io::{Result, Write}; 11 | 12 | impl<'a> RequestRender<'a> { 13 | pub fn new(workspace: &'a Workspace, headers: &'a HeaderMap, theme: &'a dyn Theme, style_enabled: bool) -> Self { 14 | Self { 15 | workspace, 16 | headers, 17 | theme, 18 | style_enabled, 19 | req_number: 0, 20 | } 21 | } 22 | } 23 | 24 | impl<'a> Render for RequestRender<'a> { 25 | #[inline] 26 | fn write(&self, writer: &mut W) -> Result<()> 27 | where 28 | W: Write, 29 | { 30 | let ws = self.workspace; 31 | let flags = ws.flags; 32 | if flags.show_request_url { 33 | if flags.show_direction { 34 | self.write_direction(writer, true)?; 35 | } 36 | self.write_method(writer)?; 37 | writer.write_all(b" ")?; 38 | self.write_url(writer)?; 39 | self.write_newline(writer)?; 40 | } 41 | if flags.show_request_headers { 42 | self.write_headers(writer)?; 43 | } 44 | if flags.show_request_body { 45 | self.write_body(writer)?; 46 | } 47 | Ok(()) 48 | } 49 | 50 | #[inline] 51 | fn is_style_active(&self) -> bool { 52 | self.style_enabled 53 | } 54 | } 55 | 56 | impl<'a> RequestRender<'a> { 57 | #[inline] 58 | fn write_direction(&self, writer: &mut W, is_standard: bool) -> Result<()> { 59 | self.write_with_style(writer, DIRECTION_REQUEST, &self.theme.request().direction(is_standard)) 60 | } 61 | 62 | #[inline] 63 | fn write_method(&self, writer: &mut W) -> Result<()> { 64 | let ws = self.workspace; 65 | self.write_with_style(writer, ws.method.as_str().as_bytes(), &self.theme.request().method()) 66 | } 67 | 68 | #[inline] 69 | fn write_url(&self, writer: &mut W) -> Result<()> { 70 | let ws = self.workspace; 71 | let urls: &[String] = &ws.urls; 72 | 73 | self.write_with_style( 74 | writer, 75 | if urls.len() > self.req_number { &urls[self.req_number] } else { "??" }.as_bytes(), 76 | &self.theme.request().url(), 77 | ) 78 | } 79 | 80 | #[inline] 81 | fn write_body(&self, writer: &mut W) -> Result<()> { 82 | let ws = self.workspace; 83 | if ws.has_items() { 84 | let flags = ws.flags; 85 | let items = ws.items.borrow(); 86 | if ws.is_json() { 87 | let json_render = JsonRender::new(&items as &Items, flags.show_request_compact, self.style_enabled); 88 | json_render.write(writer)?; 89 | } else { 90 | let json_render = FormRender::new(&items as &Items, flags.show_request_compact, self.style_enabled); 91 | json_render.write(writer)?; 92 | } 93 | self.write_newline(writer)?; 94 | } else if let Some(ref raw) = ws.raw { 95 | writer.write_all(raw.as_bytes())?; 96 | self.write_newline(writer)?; 97 | } 98 | Ok(()) 99 | } 100 | 101 | #[inline] 102 | fn write_headers(&self, writer: &mut W) -> Result<()> { 103 | let request_theme = self.theme.request(); 104 | let header_theme = request_theme.as_header(); 105 | let direction_theme = request_theme.as_direction(); 106 | let header_render = HeaderRender::new(self.workspace, self.headers, header_theme, direction_theme, DIRECTION_REQUEST, self.style_enabled); 107 | header_render.write(writer) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /crates/cli/src/commands/http/render/response.rs: -------------------------------------------------------------------------------- 1 | use super::{HeaderRender, Render, ResponseRender, DIRECTION_RESPONSE}; 2 | use crate::request::Response; 3 | use crate::rh_name; 4 | use crate::shell::json::JsonRender; 5 | use crate::{core::Workspace, theme::Theme}; 6 | use content_inspector::inspect; 7 | use serde_json::Value; 8 | use std::cell::RefCell; 9 | use std::io::Read; 10 | use std::io::{Result, Write}; 11 | 12 | impl<'a> ResponseRender<'a> { 13 | pub fn new(workspace: &'a Workspace, response: RefCell, theme: &'a dyn Theme, style_enabled: bool) -> Self { 14 | Self { 15 | workspace, 16 | response, 17 | theme, 18 | style_enabled, 19 | } 20 | } 21 | } 22 | 23 | impl<'a> Render for ResponseRender<'a> { 24 | #[inline] 25 | fn write(&self, writer: &mut W) -> Result<()> 26 | where 27 | W: Write, 28 | { 29 | let ws = self.workspace; 30 | let flags = ws.flags; 31 | if flags.show_response_status { 32 | if flags.show_direction { 33 | self.write_direction(writer, true)?; 34 | } 35 | self.write_version_and_status(writer)?; 36 | } 37 | if flags.show_response_headers { 38 | self.write_headers(writer)?; 39 | } 40 | if flags.show_response_body { 41 | self.write_body(writer)?; 42 | } 43 | Ok(()) 44 | } 45 | 46 | #[inline] 47 | fn is_style_active(&self) -> bool { 48 | self.style_enabled 49 | } 50 | } 51 | 52 | impl<'a> ResponseRender<'a> { 53 | #[inline] 54 | fn write_direction(&self, writer: &mut W, is_standard: bool) -> Result<()> { 55 | self.write_with_style(writer, DIRECTION_RESPONSE, &self.theme.response().direction(is_standard)) 56 | } 57 | 58 | #[inline] 59 | fn write_version_and_status(&self, writer: &mut W) -> Result<()> { 60 | let response = &self.response.borrow(); 61 | let status = response.status(); 62 | let theme = self.workspace.theme.response(); 63 | 64 | let style = theme.version(); 65 | let message = format!("{:?} ", response.version()); 66 | self.write_with_style(writer, message.as_bytes(), &style)?; 67 | 68 | let style = theme.status(); 69 | self.write_with_style(writer, status.as_str().as_bytes(), &style)?; 70 | writer.write_all(b" ")?; 71 | self.write_with_style(writer, status.canonical_reason().unwrap_or("Unknown").as_bytes(), &style)?; 72 | self.write_newline(writer) 73 | } 74 | 75 | #[inline] 76 | fn write_body(&self, writer: &mut W) -> Result<()> { 77 | let ws = self.workspace; 78 | let flags = ws.flags; 79 | let mut response = self.response.borrow_mut(); 80 | 81 | let mut bytes = Vec::new(); 82 | let size = response.read_to_end(&mut bytes).unwrap_or(0); 83 | let content_type = inspect(&bytes); 84 | if content_type.is_binary() { 85 | self.write_binary_usage(writer, size)?; 86 | } else { 87 | let body = String::from_utf8_lossy(&bytes); 88 | match serde_json::from_str::(&body) { 89 | Ok(json) => { 90 | let json_render = JsonRender::new(&json, flags.show_response_compact, self.style_enabled); 91 | json_render.write(writer)?; 92 | } 93 | Err(_) => { 94 | writer.write_all(body.as_bytes())?; 95 | } 96 | } 97 | } 98 | self.write_newline(writer) 99 | } 100 | 101 | #[inline] 102 | fn write_binary_usage(&self, writer: &mut W, size: usize) -> Result<()> { 103 | let message = format!( 104 | "Binary data not shown in terminal\nContent size {}b\nTo copy the content in a file, you should try:\n{} > filename", 105 | size, 106 | rh_name!() 107 | ); 108 | writer.write_all(message.as_bytes())?; 109 | self.write_newline(writer)?; 110 | Ok(()) 111 | } 112 | 113 | #[inline] 114 | fn write_headers(&self, writer: &mut W) -> Result<()> { 115 | let response = self.response.borrow(); 116 | let headers = response.headers(); 117 | let response_theme = self.theme.response(); 118 | let header_theme = response_theme.as_header(); 119 | let direction_theme = response_theme.as_direction(); 120 | let header_render = HeaderRender::new(self.workspace, headers, header_theme, direction_theme, DIRECTION_RESPONSE, self.style_enabled); 121 | header_render.write(writer) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /crates/cli/src/commands/http/version.rs: -------------------------------------------------------------------------------- 1 | pub fn show() { 2 | println!("{}", crate::rh_version!()); 3 | } 4 | -------------------------------------------------------------------------------- /crates/cli/src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "alias")] 2 | pub(crate) mod alias; 3 | pub(crate) mod args; 4 | mod debug; 5 | pub(crate) mod http; 6 | 7 | use crate::{ 8 | core::{Args, Result}, 9 | shell::Shell, 10 | }; 11 | 12 | type DonePtr = fn(); 13 | 14 | #[cfg(feature = "alias")] 15 | const ALIAS_NAME_PREFIX: char = '@'; 16 | 17 | pub trait ArgsCommand { 18 | fn command(&mut self, os_dirs: &OD) -> Result>>; 19 | } 20 | 21 | pub trait Command { 22 | fn execute(&self, shell: &mut Shell, args: &mut Args, done: DonePtr) -> Result<()>; 23 | } 24 | -------------------------------------------------------------------------------- /crates/cli/src/core/error.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "alias")] 2 | use crate::commands::alias::Error as AliasError; 3 | 4 | #[cfg_attr(test, derive(Debug))] 5 | #[derive(PartialEq)] 6 | pub enum Error { 7 | NoArgs, 8 | MissingUrl, 9 | ItemsAndRawMix, 10 | TooManyRaw, 11 | ContradictoryScheme, 12 | Unexpected(String), 13 | InvalidFlag(String), 14 | InvalidHeader(String), 15 | InvalidItem(String), 16 | BadHeaderName(String), 17 | BadHeaderValue(String), 18 | Request(String), 19 | Io(String), 20 | #[cfg(feature = "alias")] 21 | AliasCommand(AliasError), 22 | #[cfg(feature = "alias")] 23 | Alias(String), 24 | #[cfg(feature = "alias")] 25 | AliasOther, // FIXME To be removed 26 | } 27 | -------------------------------------------------------------------------------- /crates/cli/src/core/flags.rs: -------------------------------------------------------------------------------- 1 | #[cfg_attr(test, derive(Debug))] 2 | #[derive(Clone, Copy)] 3 | pub struct Flags { 4 | pub show_version: bool, 5 | pub show_help: bool, 6 | pub show_short_help: bool, 7 | pub debug: bool, 8 | 9 | pub https: bool, 10 | pub http: bool, 11 | pub use_color: bool, 12 | pub show_direction: bool, 13 | 14 | pub as_json: bool, 15 | pub as_form: bool, 16 | 17 | pub show_request_url: bool, 18 | pub show_request_headers: bool, 19 | pub show_request_compact: bool, 20 | pub show_request_body: bool, 21 | 22 | pub show_response_status: bool, 23 | pub show_response_headers: bool, 24 | pub show_response_compact: bool, 25 | pub show_response_body: bool, 26 | } 27 | -------------------------------------------------------------------------------- /crates/cli/src/core/mod.rs: -------------------------------------------------------------------------------- 1 | mod error; 2 | mod flags; 3 | mod types; 4 | mod workspace; 5 | 6 | pub use error::Error; 7 | pub use flags::Flags; 8 | pub use types::{Args, HeaderMap, Mode, Result}; 9 | pub use workspace::Workspace; 10 | 11 | pub trait PushDataItem { 12 | fn push(&mut self, item: &str) -> Result<()>; 13 | } 14 | 15 | pub trait WorkspaceData { 16 | fn is_json(&self) -> bool; 17 | fn is_form(&self) -> bool; 18 | fn has_items(&self) -> bool; 19 | } 20 | -------------------------------------------------------------------------------- /crates/cli/src/core/types.rs: -------------------------------------------------------------------------------- 1 | use super::error::Error; 2 | 3 | pub type Args = Vec; 4 | pub type HeaderMap = reqwest::header::HeaderMap; 5 | pub type Result = std::result::Result; 6 | 7 | #[cfg_attr(test, derive(Debug, PartialEq))] 8 | pub enum Mode { 9 | Run, 10 | Help, 11 | Version, 12 | Debug, 13 | } 14 | -------------------------------------------------------------------------------- /crates/cli/src/core/workspace.rs: -------------------------------------------------------------------------------- 1 | use crate::items::Items; 2 | use crate::request::Method; 3 | use crate::theme::Theme; 4 | use std::cell::RefCell; 5 | 6 | use super::{Flags, HeaderMap, Mode}; 7 | 8 | #[cfg_attr(test, derive(Debug))] 9 | pub struct Workspace { 10 | pub method: Method, 11 | pub urls: Vec, 12 | pub output_redirected: bool, 13 | pub terminal_columns: u16, 14 | pub theme: Box, // FIXME Create a crate for theme 15 | pub flags: Flags, 16 | pub headers: RefCell, 17 | pub items: RefCell, 18 | pub raw: Option, 19 | pub certificate_authority_file: Option, 20 | } 21 | 22 | impl Workspace { 23 | pub fn mode(&self) -> Mode { 24 | if self.flags.show_help || self.flags.show_short_help { 25 | Mode::Help 26 | } else if self.flags.show_version { 27 | Mode::Version 28 | } else if self.flags.debug { 29 | Mode::Debug 30 | } else { 31 | Mode::Run 32 | } 33 | } 34 | } 35 | 36 | impl super::WorkspaceData for Workspace { 37 | fn is_json(&self) -> bool { 38 | self.flags.as_json || (!self.flags.as_form && self.has_items()) 39 | } 40 | fn is_form(&self) -> bool { 41 | self.flags.as_form 42 | } 43 | fn has_items(&self) -> bool { 44 | self.items.borrow().len() > 0 45 | } 46 | } 47 | 48 | // UNIT TESTS ///////////////////////////////////////////////////////////////////////////// 49 | 50 | #[cfg(test)] 51 | mod tests { 52 | mod workspace { 53 | use crate::{ 54 | core::{Flags, HeaderMap, Mode, PushDataItem, Workspace, WorkspaceData}, 55 | items::Items, 56 | request::Method, 57 | theme::default::DefaultTheme, 58 | }; 59 | use std::cell::RefCell; 60 | 61 | #[test] 62 | fn json_flag() { 63 | let args = Workspace { 64 | method: Method::GET, 65 | urls: Vec::new(), 66 | output_redirected: false, 67 | terminal_columns: 100, 68 | theme: Box::new(DefaultTheme {}), 69 | flags: Flags { 70 | as_json: true, 71 | ..Flags::default() 72 | }, 73 | headers: RefCell::new(HeaderMap::new()), 74 | items: RefCell::new(Items::new()), 75 | raw: None, 76 | certificate_authority_file: None, 77 | }; 78 | assert_eq!(args.is_json(), true); 79 | assert_eq!(args.has_items(), false); 80 | assert_eq!(args.is_form(), false); 81 | assert_eq!(args.mode(), Mode::Run); 82 | } 83 | 84 | #[test] 85 | fn json_items() { 86 | let mut items = Items::new(); 87 | let _ = items.push("key=value"); 88 | let args = Workspace { 89 | method: Method::GET, 90 | urls: Vec::new(), 91 | output_redirected: false, 92 | terminal_columns: 100, 93 | theme: Box::new(DefaultTheme {}), 94 | flags: Flags { 95 | as_json: false, 96 | ..Flags::default() 97 | }, 98 | headers: RefCell::new(HeaderMap::new()), 99 | items: RefCell::new(items), 100 | raw: None, 101 | certificate_authority_file: None, 102 | }; 103 | assert_eq!(args.is_json(), true); 104 | assert_eq!(args.has_items(), true); 105 | assert_eq!(args.is_form(), false); 106 | assert_eq!(args.mode(), Mode::Run); 107 | } 108 | 109 | #[test] 110 | fn form_flag() { 111 | let args = Workspace { 112 | method: Method::GET, 113 | urls: Vec::new(), 114 | output_redirected: false, 115 | terminal_columns: 100, 116 | theme: Box::new(DefaultTheme {}), 117 | flags: Flags { 118 | as_form: true, 119 | ..Flags::default() 120 | }, 121 | headers: RefCell::new(HeaderMap::new()), 122 | items: RefCell::new(Items::new()), 123 | raw: None, 124 | certificate_authority_file: None, 125 | }; 126 | assert_eq!(args.is_json(), false); 127 | assert_eq!(args.has_items(), false); 128 | assert_eq!(args.is_form(), true); 129 | assert_eq!(args.mode(), Mode::Run); 130 | } 131 | 132 | #[test] 133 | fn version() { 134 | let args = Workspace { 135 | method: Method::GET, 136 | urls: Vec::new(), 137 | output_redirected: false, 138 | terminal_columns: 100, 139 | theme: Box::new(DefaultTheme {}), 140 | flags: Flags { 141 | show_version: true, 142 | ..Flags::default() 143 | }, 144 | headers: RefCell::new(HeaderMap::new()), 145 | items: RefCell::new(Items::new()), 146 | raw: None, 147 | certificate_authority_file: None, 148 | }; 149 | assert_eq!(args.mode(), Mode::Version); 150 | } 151 | 152 | #[test] 153 | fn help() { 154 | let args = Workspace { 155 | method: Method::GET, 156 | urls: Vec::new(), 157 | output_redirected: false, 158 | terminal_columns: 100, 159 | theme: Box::new(DefaultTheme {}), 160 | flags: Flags { 161 | show_help: true, 162 | ..Flags::default() 163 | }, 164 | headers: RefCell::new(HeaderMap::new()), 165 | items: RefCell::new(Items::new()), 166 | raw: None, 167 | certificate_authority_file: None, 168 | }; 169 | assert_eq!(args.mode(), Mode::Help); 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /crates/cli/src/items/mod.rs: -------------------------------------------------------------------------------- 1 | mod number; 2 | mod ser; 3 | mod value; 4 | 5 | use crate::core::{Error, PushDataItem}; 6 | use std::collections::HashMap; 7 | use value::Value; 8 | 9 | pub type Items = HashMap; 10 | 11 | const FORCE_STRING: &str = "/"; 12 | 13 | impl PushDataItem for Items { 14 | fn push(&mut self, item: &str) -> Result<(), Error> { 15 | match item.split_once("=") { 16 | Some(parts) => { 17 | let key = parts.0.to_string(); 18 | if key.ends_with(FORCE_STRING) { 19 | self.insert(key[..(key.len() - 1)].to_string(), Value::String(parts.1.to_string())) 20 | } else { 21 | self.insert(key, parts.1.into()) 22 | } 23 | } 24 | None => return Err(Error::InvalidItem(item.into())), 25 | }; 26 | Ok(()) 27 | } 28 | } 29 | 30 | // UNIT TESTS ///////////////////////////////////////////////////////////////////////////// 31 | 32 | #[cfg(test)] 33 | mod tests { 34 | use super::{Items, PushDataItem, Value, FORCE_STRING}; 35 | 36 | macro_rules! assert_item_eq { 37 | ($item:expr, $key:expr, $value:expr) => { 38 | let mut items = Items::new(); 39 | let _ = items.push($item.into()); 40 | 41 | assert_eq!(items.len(), 1); 42 | assert_eq!(items.get(&$key.to_string()), Some(&$value)) 43 | }; 44 | } 45 | 46 | macro_rules! key_value_force_string { 47 | ($key:expr, $value:expr) => { 48 | format!("{}{}={}", $key, FORCE_STRING, $value).as_str() 49 | }; 50 | } 51 | 52 | macro_rules! value_bool { 53 | ($value:expr) => { 54 | Value::Bool($value) 55 | }; 56 | } 57 | macro_rules! value_string { 58 | ($value:expr) => { 59 | Value::String($value.to_string()) 60 | }; 61 | } 62 | macro_rules! value_number { 63 | ($value:expr) => { 64 | Value::Number($value.into()) 65 | }; 66 | } 67 | 68 | #[test] 69 | fn types() { 70 | assert_item_eq!("key=value", "key", value_string!("value")); 71 | assert_item_eq!("key=true", "key", value_bool!(true)); 72 | assert_item_eq!("key=y", "key", value_bool!(true)); 73 | assert_item_eq!("key=false", "key", value_bool!(false)); 74 | assert_item_eq!("key=n", "key", value_bool!(false)); 75 | 76 | assert_item_eq!("k|e|y=$true", "k|e|y", value_string!("$true")); 77 | assert_item_eq!("k.e.y=$false", "k.e.y", value_string!("$false")); 78 | assert_item_eq!(key_value_force_string!("k|e|y", "true"), "k|e|y", value_string!("true")); 79 | assert_item_eq!(key_value_force_string!("k|e|y", "y"), "k|e|y", value_string!("y")); 80 | assert_item_eq!(key_value_force_string!("k.e.y", "false"), "k.e.y", value_string!("false")); 81 | assert_item_eq!(key_value_force_string!("k|e|y", "n"), "k|e|y", value_string!("n")); 82 | assert_item_eq!(key_value_force_string!("@key", "hello"), "@key", value_string!("hello")); 83 | assert_item_eq!(key_value_force_string!("@key$", "hello"), "@key$", value_string!("hello")); 84 | 85 | assert_item_eq!("a=1", "a", value_number!(1)); 86 | assert_item_eq!("bc=123", "bc", value_number!(123)); 87 | assert_item_eq!("d-e=123.456", "d-e", value_number!((123.456))); 88 | assert_item_eq!("f_g=-123.456", "f_g", value_number!((-123.456))); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /crates/cli/src/items/number.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | use std::fmt::{self, Debug, Display}; 3 | use std::i64; 4 | 5 | use serde::{Serialize, Serializer}; 6 | 7 | #[derive(Clone, PartialEq, PartialOrd)] 8 | pub struct Number { 9 | n: N, 10 | } 11 | 12 | impl fmt::Display for Number { 13 | fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 14 | match self.n { 15 | N::PosInt(i) => Display::fmt(&i, formatter), 16 | N::NegInt(i) => Display::fmt(&i, formatter), 17 | N::Float(f) if f.is_nan() => formatter.write_str(".nan"), 18 | N::Float(f) if f.is_infinite() => { 19 | if f.is_sign_negative() { 20 | formatter.write_str("-.inf") 21 | } else { 22 | formatter.write_str(".inf") 23 | } 24 | } 25 | N::Float(f) => Display::fmt(&f, formatter), 26 | } 27 | } 28 | } 29 | 30 | impl Debug for Number { 31 | fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 32 | Debug::fmt(&self.n, formatter) 33 | } 34 | } 35 | 36 | #[derive(Copy, Clone, Debug)] 37 | enum N { 38 | PosInt(u64), 39 | NegInt(i64), 40 | Float(f64), 41 | } 42 | 43 | impl Number { 44 | #[inline] 45 | pub fn from_str(n: &str) -> Option { 46 | match n.parse::() { 47 | Ok(num) => Some(Number { n: N::PosInt(num) }), 48 | _ => match n.parse::() { 49 | Ok(num) => Some(Number { n: N::NegInt(num) }), 50 | _ => match n.parse::() { 51 | Ok(num) => Some(Number { n: N::Float(num) }), 52 | _ => None, 53 | }, 54 | }, 55 | } 56 | } 57 | } 58 | 59 | impl PartialEq for N { 60 | fn eq(&self, other: &N) -> bool { 61 | match (*self, *other) { 62 | (N::PosInt(a), N::PosInt(b)) => a == b, 63 | (N::NegInt(a), N::NegInt(b)) => a == b, 64 | (N::Float(a), N::Float(b)) => { 65 | if a.is_nan() && b.is_nan() { 66 | true 67 | } else { 68 | a == b 69 | } 70 | } 71 | _ => false, 72 | } 73 | } 74 | } 75 | 76 | impl PartialOrd for N { 77 | fn partial_cmp(&self, other: &Self) -> Option { 78 | match (*self, *other) { 79 | (N::Float(a), N::Float(b)) => { 80 | if a.is_nan() && b.is_nan() { 81 | Some(Ordering::Equal) 82 | } else { 83 | a.partial_cmp(&b) 84 | } 85 | } 86 | _ => Some(self.total_cmp(other)), 87 | } 88 | } 89 | } 90 | 91 | impl N { 92 | fn total_cmp(&self, other: &Self) -> Ordering { 93 | match (*self, *other) { 94 | (N::PosInt(a), N::PosInt(b)) => a.cmp(&b), 95 | (N::NegInt(a), N::NegInt(b)) => a.cmp(&b), 96 | (N::NegInt(_), N::PosInt(_)) => Ordering::Less, 97 | (N::PosInt(_), N::NegInt(_)) => Ordering::Greater, 98 | (N::Float(a), N::Float(b)) => a.partial_cmp(&b).unwrap_or_else(|| { 99 | if !a.is_nan() { 100 | Ordering::Less 101 | } else if !b.is_nan() { 102 | Ordering::Greater 103 | } else { 104 | Ordering::Equal 105 | } 106 | }), 107 | (_, N::Float(_)) => Ordering::Less, 108 | (N::Float(_), _) => Ordering::Greater, 109 | } 110 | } 111 | } 112 | 113 | impl From for Number { 114 | #[inline] 115 | fn from(f: f64) -> Self { 116 | let n = { N::Float(f) }; 117 | Number { n } 118 | } 119 | } 120 | 121 | impl Serialize for Number { 122 | #[inline] 123 | fn serialize(&self, serializer: S) -> Result 124 | where 125 | S: Serializer, 126 | { 127 | match self.n { 128 | N::PosInt(u) => serializer.serialize_u64(u), 129 | N::NegInt(i) => serializer.serialize_i64(i), 130 | N::Float(f) => serializer.serialize_f64(f), 131 | } 132 | } 133 | } 134 | 135 | macro_rules! impl_from_unsigned { 136 | ( 137 | $($ty:ty),* 138 | ) => { 139 | $( 140 | impl From<$ty> for Number { 141 | #[inline] 142 | fn from(u: $ty) -> Self { 143 | let n = { 144 | N::PosInt(u as u64) 145 | }; 146 | Number { n } 147 | } 148 | } 149 | )* 150 | }; 151 | } 152 | 153 | macro_rules! impl_from_signed { 154 | ( 155 | $($ty:ty),* 156 | ) => { 157 | $( 158 | impl From<$ty> for Number { 159 | #[inline] 160 | fn from(i: $ty) -> Self { 161 | let n = { 162 | if i < 0 { 163 | N::NegInt(i as i64) 164 | } else { 165 | N::PosInt(i as u64) 166 | } 167 | }; 168 | Number { n } 169 | } 170 | } 171 | )* 172 | }; 173 | } 174 | 175 | impl_from_unsigned!(u8, u16, u32, u64, usize); 176 | impl_from_signed!(i8, i16, i32, i64, isize); 177 | 178 | // UNIT TESTS ///////////////////////////////////////////////////////////////////////////// 179 | 180 | #[cfg(test)] 181 | mod tests { 182 | use super::Number; 183 | 184 | #[test] 185 | fn detect_numbers_from_str() { 186 | assert_eq!(Number::from_str("123"), Some(123u64.into())); 187 | assert_eq!(Number::from_str("18446744073709551615"), Some(18446744073709551615u64.into())); 188 | assert_eq!(Number::from_str("18446744073709551615118446744073709551615"), Some(1.8446744073709552e40f64.into())); 189 | 190 | assert_eq!(Number::from_str("-123"), Some((-123).into())); 191 | assert_eq!(Number::from_str("-9223372036854775807"), Some((-9223372036854775807i64).into())); 192 | 193 | assert_eq!(Number::from_str("123.456"), Some((123.456f64).into())); 194 | assert_eq!(Number::from_str("18446744073709551615.456"), Some((18446744073709551615.456f64).into())); 195 | assert_eq!(Number::from_str("123e10"), Some((1230000000000.0f64).into())); 196 | 197 | assert_eq!(Number::from_str("a123"), None); 198 | assert_eq!(Number::from_str("123.a"), None); 199 | assert_eq!(Number::from_str("123e"), None); 200 | assert_eq!(Number::from_str("hello"), None); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /crates/cli/src/items/ser.rs: -------------------------------------------------------------------------------- 1 | use super::value::Value; 2 | use serde::ser::Serialize; 3 | 4 | impl Serialize for Value { 5 | #[inline] 6 | fn serialize(&self, serializer: S) -> Result 7 | where 8 | S: ::serde::Serializer, 9 | { 10 | match *self { 11 | Value::Null => serializer.serialize_unit(), 12 | Value::Bool(b) => serializer.serialize_bool(b), 13 | Value::Number(ref n) => n.serialize(serializer), 14 | Value::String(ref s) => serializer.serialize_str(s), 15 | // Value::Array(ref v) => v.serialize(serializer), 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /crates/cli/src/items/value.rs: -------------------------------------------------------------------------------- 1 | use super::number::Number; 2 | 3 | #[derive(Clone, PartialEq, Debug)] 4 | pub enum Value { 5 | Null, 6 | Bool(bool), 7 | Number(Number), 8 | String(String), 9 | // Array(Vec), 10 | } 11 | 12 | impl<'a> From<&'a str> for Value { 13 | fn from(value: &str) -> Self { 14 | match value { 15 | "true" | "y" => Value::Bool(true), 16 | "false" | "n" => Value::Bool(false), 17 | "" => Value::Null, 18 | _ => match Number::from_str(value) { 19 | Some(num) => Value::Number(num), 20 | _ => Value::String(value.to_string()), 21 | }, 22 | } 23 | } 24 | } 25 | 26 | // UNIT TESTS ///////////////////////////////////////////////////////////////////////////// 27 | 28 | #[cfg(test)] 29 | mod tests { 30 | use super::Value; 31 | 32 | macro_rules! assert_value_eq { 33 | ($value:expr, $expected:expr) => { 34 | let value: Value = $value.into(); 35 | assert_eq!(value, $expected) 36 | }; 37 | } 38 | 39 | macro_rules! value_string { 40 | ($value:expr) => { 41 | Value::String($value.to_string()) 42 | }; 43 | } 44 | macro_rules! value_number { 45 | ($value:expr) => { 46 | Value::Number($value.into()) 47 | }; 48 | } 49 | 50 | #[test] 51 | fn detect_type_from_str() { 52 | assert_value_eq!("true", Value::Bool(true)); 53 | assert_value_eq!("y", Value::Bool(true)); 54 | assert_value_eq!("false", Value::Bool(false)); 55 | assert_value_eq!("n", Value::Bool(false)); 56 | 57 | assert_value_eq!("$", value_string!("$")); 58 | assert_value_eq!("hello", value_string!("hello")); 59 | 60 | assert_value_eq!("1", value_number!(1)); 61 | assert_value_eq!("123", value_number!(123)); 62 | assert_value_eq!("123.456", value_number!((123.456))); 63 | assert_value_eq!("-123.456", value_number!((-123.456))); 64 | 65 | assert_value_eq!("", Value::Null); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /crates/cli/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod app; 2 | mod commands; 3 | mod core; 4 | mod items; 5 | mod macros; 6 | mod parser; 7 | mod request; 8 | pub mod shell; 9 | #[cfg(test)] 10 | pub mod test; 11 | mod theme; 12 | 13 | use crate::app::App; 14 | use crate::shell::os::OsDirs; 15 | use crate::shell::Shell; 16 | use std::io::Write; 17 | 18 | #[inline] 19 | pub fn run<'a, OD: OsDirs, O: Write, E: Write>(args: &mut Vec, shell: &'a mut Shell<'a, OD, O, E>) -> i32 { 20 | let mut app = App::new(shell); 21 | match app.run(args) { 22 | Ok(_) => app.exit_code(None), 23 | Err(err) => app.exit_code(Some(err)), 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /crates/cli/src/macros.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! ifelse { 3 | ($condition: expr, $_true: expr, $_false: expr) => { 4 | if $condition { 5 | $_true 6 | } else { 7 | $_false 8 | } 9 | }; 10 | } 11 | 12 | #[macro_export] 13 | macro_rules! rh_name { 14 | () => { 15 | env!("CARGO_PKG_NAME") 16 | }; 17 | } 18 | 19 | #[macro_export] 20 | macro_rules! rh_version { 21 | () => { 22 | env!("CARGO_PKG_VERSION") 23 | }; 24 | } 25 | 26 | #[macro_export] 27 | macro_rules! rh_homepage { 28 | () => { 29 | env!("CARGO_PKG_HOMEPAGE") 30 | }; 31 | } 32 | 33 | #[cfg(test)] 34 | mod tests { 35 | #[test] 36 | fn ifelse() { 37 | let res = ifelse![true, true, false]; 38 | assert_eq!(res, true); 39 | 40 | let res = ifelse![false, true, false]; 41 | assert_eq!(res, false); 42 | 43 | let val = true; 44 | let res = ifelse![val, 1, 2]; 45 | assert_eq!(res, 1); 46 | 47 | let val = false; 48 | let res = ifelse![val, 1, 2]; 49 | assert_eq!(res, 2); 50 | 51 | let val = 2; 52 | let res = ifelse![val == 2, "yes", "no"]; 53 | assert_eq!(res, "yes"); 54 | 55 | let val = 3; 56 | let res = ifelse![val == 2, "yes", "no"]; 57 | assert_eq!(res, "no"); 58 | } 59 | 60 | #[test] 61 | fn crate_name() { 62 | assert_eq!(rh_name!(), "rh"); 63 | } 64 | 65 | #[test] 66 | fn rh_version() { 67 | assert_eq!(rh_version!(), env!("CARGO_PKG_VERSION")); 68 | } 69 | 70 | #[test] 71 | fn rh_homepage() { 72 | assert_eq!(rh_homepage!(), env!("CARGO_PKG_HOMEPAGE")); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /crates/cli/src/main.rs: -------------------------------------------------------------------------------- 1 | use rh::shell::os::DefaultOsDirs; 2 | use rh::shell::Shell; 3 | use std::env; 4 | use std::io; 5 | use std::process::exit; 6 | 7 | fn main() { 8 | let mut os_args = env::args().skip(1).collect::>(); 9 | 10 | // let stdout = io::stdout(); 11 | // let out = io::BufWriter::new(stdout.lock()); 12 | // let stderr = io::stderr(); 13 | // let err = io::BufWriter::new(stderr.lock()); 14 | // let mut shell = Shell::new(out, err); 15 | 16 | let out = io::stdout(); 17 | let err = io::stderr(); 18 | let os_dirs = DefaultOsDirs::default(); 19 | let mut shell = Shell::new(&os_dirs, out, err); 20 | 21 | let exit_code = rh::run(&mut os_args, &mut shell); 22 | exit(exit_code); 23 | } 24 | -------------------------------------------------------------------------------- /crates/cli/src/parser/core.rs: -------------------------------------------------------------------------------- 1 | use regex::Regex; 2 | 3 | pub const RAW_FLAG: &str = "--raw="; 4 | pub const CAFILE_FLAG: &str = "--cafile="; 5 | 6 | pub trait ArgDetection { 7 | fn is_raw_flag(&self) -> bool; 8 | fn is_cafile_flag(&self) -> bool; 9 | fn is_flag(&self) -> bool; 10 | fn is_header(&self) -> bool; 11 | fn is_item(&self) -> bool; 12 | 13 | fn is_likely_url(&self) -> bool; 14 | fn is_very_likely_url(&self) -> bool; 15 | fn is_url(&self) -> bool; 16 | } 17 | 18 | impl ArgDetection for String { 19 | fn is_raw_flag(&self) -> bool { 20 | self.starts_with(RAW_FLAG) 21 | } 22 | fn is_cafile_flag(&self) -> bool { 23 | self.starts_with(CAFILE_FLAG) 24 | } 25 | fn is_flag(&self) -> bool { 26 | self.starts_with('-') 27 | } 28 | fn is_header(&self) -> bool { 29 | match self.chars().next() { 30 | Some(first_char) => first_char.is_ascii_alphanumeric() && self.contains(':') && !self.contains('='), 31 | None => false, 32 | } 33 | } 34 | fn is_item(&self) -> bool { 35 | !self.starts_with('=') && !self.starts_with('/') && !self.starts_with(':') && self.contains('=') 36 | } 37 | 38 | fn is_likely_url(&self) -> bool { 39 | !self.is_flag() 40 | } 41 | fn is_very_likely_url(&self) -> bool { 42 | Regex::new(r"^\w+[-\.\w]*:+\d{1,5}$|^\w+[-\.\w]*$").unwrap().is_match(self) 43 | } 44 | fn is_url(&self) -> bool { 45 | // FIXME Add IPv6 and IPv4 detection 46 | self.starts_with("http://") || self.starts_with("https://") || self.starts_with(':') || self.starts_with('/') 47 | } 48 | } 49 | 50 | // UNIT TESTS ///////////////////////////////////////////////////////////////////////////// 51 | 52 | #[cfg(test)] 53 | mod tests { 54 | use super::ArgDetection; 55 | 56 | macro_rules! arg { 57 | ($val:expr) => { 58 | String::from($val) 59 | }; 60 | } 61 | 62 | #[test] 63 | fn raw_flag() { 64 | assert!(arg!("--raw=").is_raw_flag()); 65 | assert!(arg!("--raw=data").is_raw_flag()); 66 | } 67 | #[test] 68 | fn not_raw_flag() { 69 | assert!(!arg!("--raw").is_raw_flag()); 70 | assert!(!arg!("-raw").is_raw_flag()); 71 | assert!(!arg!("-raw=").is_raw_flag()); 72 | assert!(!arg!("-raw=data").is_raw_flag()); 73 | } 74 | 75 | #[test] 76 | fn ca_flag() { 77 | assert!(arg!("--cafile=").is_cafile_flag()); 78 | assert!(arg!("--cafile=path").is_cafile_flag()); 79 | } 80 | #[test] 81 | fn not_ca_flag() { 82 | assert!(!arg!("--cafile").is_cafile_flag()); 83 | assert!(!arg!("-cafile").is_cafile_flag()); 84 | assert!(!arg!("-cafile=").is_cafile_flag()); 85 | assert!(!arg!("-cafile=data").is_cafile_flag()); 86 | } 87 | 88 | #[test] 89 | fn flag() { 90 | assert!(arg!("-").is_flag()); 91 | assert!(arg!("-raw").is_flag()); 92 | assert!(arg!("--raw=").is_flag()); 93 | assert!(arg!("--raw=data").is_flag()); 94 | assert!(arg!("-aBc").is_flag()); 95 | } 96 | #[test] 97 | fn not_flag() { 98 | assert!(!arg!("not-a-flag-").is_flag()); 99 | } 100 | 101 | #[test] 102 | fn header() { 103 | assert!(arg!("Key:Value").is_header()); 104 | assert!(arg!("Key-1:Value/hello/bye").is_header()); 105 | } 106 | #[test] 107 | fn not_header() { 108 | assert!(!arg!(".Key:Value").is_header()); 109 | assert!(!arg!(":Key:Value").is_header()); 110 | assert!(!arg!("/Key:Value").is_header()); 111 | assert!(!arg!("Key:SubKey=Value").is_header()); 112 | } 113 | 114 | #[test] 115 | fn item() { 116 | assert!(arg!("Key=Value").is_item()); 117 | assert!(arg!(".Key=.Value").is_item()); 118 | assert!(arg!("Key=Value:SubValue").is_item()); 119 | } 120 | #[test] 121 | fn not_item() { 122 | assert!(!arg!(":Key=Value").is_item()); 123 | assert!(!arg!("/Key=Value").is_item()); 124 | assert!(!arg!("=Key=Value").is_item()); 125 | } 126 | 127 | #[test] 128 | fn likely_url() { 129 | assert!(arg!("anything").is_likely_url()); 130 | } 131 | #[test] 132 | fn not_likely_url() { 133 | assert_eq!(arg!("--a-flag-is-not-likely-an-url").is_likely_url(), false); 134 | } 135 | 136 | #[test] 137 | fn very_likely_url() { 138 | assert!(arg!("localhost").is_very_likely_url()); 139 | assert!(arg!("localhost:1").is_very_likely_url()); 140 | assert!(arg!("localhost:12").is_very_likely_url()); 141 | assert!(arg!("localhost:123").is_very_likely_url()); 142 | assert!(arg!("localhost:1234").is_very_likely_url()); 143 | assert!(arg!("localhost:12345").is_very_likely_url()); 144 | assert!(arg!("anything").is_very_likely_url()); 145 | assert!(arg!("anything:8080").is_very_likely_url()); 146 | assert!(arg!("my-hostname").is_very_likely_url()); 147 | assert!(arg!("my-hostname:12345").is_very_likely_url()); 148 | assert!(arg!("test.com").is_very_likely_url()); 149 | assert!(arg!("test.com:80").is_very_likely_url()); 150 | assert!(arg!("test.co.uk").is_very_likely_url()); 151 | assert!(arg!("test.co.uk:443").is_very_likely_url()); 152 | assert!(arg!("a.uk").is_very_likely_url()); 153 | assert!(arg!("b.uk:1").is_very_likely_url()); 154 | } 155 | 156 | #[test] 157 | fn not_very_likely_url() { 158 | assert_eq!(arg!("anything:hi").is_very_likely_url(), false); 159 | assert_eq!(arg!("anything:808055").is_very_likely_url(), false); 160 | assert_eq!(arg!("my-hostname:hello").is_very_likely_url(), false); 161 | assert_eq!(arg!("my-hostname:123456").is_very_likely_url(), false); 162 | assert_eq!(arg!(":test.com").is_very_likely_url(), false); 163 | assert_eq!(arg!("test.com:abcdef").is_very_likely_url(), false); 164 | assert_eq!(arg!("test.com:654321").is_very_likely_url(), false); 165 | assert_eq!(arg!("test.co.uk:qwerty").is_very_likely_url(), false); 166 | assert_eq!(arg!("-test.co.uk").is_very_likely_url(), false); 167 | assert_eq!(arg!(".test.co.uk").is_very_likely_url(), false); 168 | assert_eq!(arg!("@test.co.uk").is_very_likely_url(), false); 169 | assert_eq!(arg!("/test.co.uk").is_very_likely_url(), false); 170 | assert_eq!(arg!("*test.co.uk").is_very_likely_url(), false); 171 | } 172 | 173 | #[test] 174 | fn url() { 175 | assert!(arg!("http://test.com").is_url()); 176 | assert!(arg!("https://test.com").is_url()); 177 | assert!(arg!("/path/hello?r=y").is_url()); 178 | assert!(arg!("/").is_url()); 179 | assert!(arg!("/path").is_url()); 180 | assert!(arg!(":").is_url()); 181 | assert!(arg!(":9200").is_url()); 182 | } 183 | #[test] 184 | fn not_url() { 185 | assert_eq!(arg!("not-anything").is_url(), false); 186 | assert_eq!(arg!("--a-flag-is-not-an-url").is_url(), false); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /crates/cli/src/parser/error.rs: -------------------------------------------------------------------------------- 1 | use crate::core::Error; 2 | use std::io; 3 | 4 | impl From for Error { 5 | fn from(err: io::Error) -> Error { 6 | Error::Io(err.to_string()) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /crates/cli/src/parser/flags.rs: -------------------------------------------------------------------------------- 1 | use super::Error; 2 | use crate::core::Flags; 3 | use regex::Regex; 4 | 5 | impl Default for Flags { 6 | fn default() -> Self { 7 | Self { 8 | show_version: false, 9 | show_help: false, 10 | show_short_help: false, 11 | debug: false, 12 | 13 | https: false, 14 | http: false, 15 | use_color: true, 16 | show_direction: false, 17 | 18 | as_json: false, 19 | as_form: false, 20 | 21 | show_request_url: false, 22 | show_request_headers: false, 23 | show_request_compact: false, 24 | show_request_body: false, 25 | 26 | show_response_status: false, 27 | show_response_headers: false, 28 | show_response_compact: false, 29 | show_response_body: true, 30 | } 31 | } 32 | } 33 | 34 | impl Flags { 35 | pub fn new(output_redirected: bool) -> Flags { 36 | Flags { 37 | use_color: !output_redirected, 38 | ..Default::default() 39 | } 40 | } 41 | 42 | pub fn push(&mut self, flag: &str) -> Result<(), Error> { 43 | match flag { 44 | "--version" => self.show_version = true, 45 | "--help" => self.show_help = true, 46 | "--debug" => self.debug = true, 47 | "-U" | "--url" => self.show_request_url = true, 48 | "-s" | "--status" => self.show_response_status = true, 49 | "-d" | "--direction" => self.show_direction = true, 50 | "-v" | "--verbose" => self.enable_verbose(), 51 | "--pretty=c" | "--pretty=color" => self.use_color = true, 52 | "--json" => self.as_json = true, 53 | "--form" => self.as_form = true, 54 | "--http" => { 55 | self.http = true; 56 | if self.is_contradictory_scheme() { 57 | return Err(Error::ContradictoryScheme); 58 | } 59 | } 60 | "--https" | "--ssl" => { 61 | self.https = true; 62 | if self.is_contradictory_scheme() { 63 | return Err(Error::ContradictoryScheme); 64 | } 65 | } 66 | "--headers" => { 67 | self.show_request_headers = true; 68 | self.show_response_headers = true; 69 | } 70 | "-H" | "--req-headers" => self.show_request_headers = true, 71 | "-h" => { 72 | self.show_short_help = true; 73 | self.show_response_headers = true; 74 | } 75 | "--header" => self.show_response_headers = true, 76 | "-B" | "--req-body" => self.show_request_body = true, 77 | "-b" | "--body" => self.show_response_body = true, 78 | "-C" | "--req-compact" => self.show_request_compact = true, 79 | "-c" | "--compact" => self.show_response_compact = true, 80 | _ => { 81 | let has_valid_compact_flags = self.extract_compact_flags(flag); 82 | if !has_valid_compact_flags { 83 | return Err(Error::InvalidFlag(flag.to_string())); 84 | } 85 | } 86 | }; 87 | Ok(()) 88 | } 89 | 90 | fn extract_compact_flags(&mut self, flag: &str) -> bool { 91 | // FIXME Need something like "-no-bBH..." to set the related flags to false 92 | let valid = Regex::new(r"^\-[vcCdUshHbB]*$").unwrap().is_match(flag); 93 | if valid { 94 | if flag.contains('v') { 95 | self.enable_verbose(); 96 | } 97 | if flag.contains('c') { 98 | self.show_response_compact = true; 99 | } 100 | if flag.contains('C') { 101 | self.show_request_compact = true; 102 | } 103 | if flag.contains('d') { 104 | self.show_direction = true; 105 | } 106 | if flag.contains('U') { 107 | self.show_request_url = true; 108 | } 109 | if flag.contains('s') { 110 | self.show_response_status = true; 111 | } 112 | if flag.contains('H') { 113 | self.show_request_headers = true; 114 | } 115 | if flag.contains('h') { 116 | self.show_response_headers = true; 117 | } 118 | if flag.contains('b') { 119 | self.show_response_body = true; 120 | } 121 | if flag.contains('B') { 122 | self.show_request_body = true; 123 | } 124 | } 125 | valid 126 | } 127 | 128 | fn enable_verbose(&mut self) { 129 | self.show_direction = true; 130 | self.show_request_url = true; 131 | self.show_response_status = true; 132 | self.show_request_headers = true; 133 | self.show_response_headers = true; 134 | self.show_request_body = true; 135 | self.show_response_body = true; 136 | } 137 | 138 | fn is_contradictory_scheme(&self) -> bool { 139 | self.http && self.https 140 | } 141 | } 142 | 143 | // UNIT TESTS ///////////////////////////////////////////////////////////////////////////// 144 | 145 | #[cfg(test)] 146 | mod tests { 147 | use super::{Error, Flags}; 148 | 149 | macro_rules! flag { 150 | () => {{ 151 | Flags::new(false) 152 | }}; 153 | ( $( $elem:expr ),* ) => { 154 | { 155 | let mut temp_flags = Flags::new(false); 156 | $( 157 | let _ = temp_flags.push($elem); 158 | )* 159 | temp_flags 160 | } 161 | }; 162 | } 163 | 164 | #[test] 165 | fn valid_scheme() { 166 | let flags = flag!["--http"]; 167 | assert_eq!(flags.http, true); 168 | 169 | let flags = flag!["--https"]; 170 | assert_eq!(flags.https, true); 171 | } 172 | 173 | #[test] 174 | fn contradictory_scheme() { 175 | let mut flags = flag![]; 176 | let _ = flags.push("--http"); 177 | let res = flags.push("--https"); 178 | assert!(res.is_err()); 179 | assert_eq!(res.unwrap_err(), Error::ContradictoryScheme); 180 | 181 | let mut flags = flag![]; 182 | let _ = flags.push("--https"); 183 | let res = flags.push("--http"); 184 | assert!(res.is_err()); 185 | assert_eq!(res.unwrap_err(), Error::ContradictoryScheme); 186 | 187 | let mut flags = flag!["-H", "-h"]; 188 | let _ = flags.push("--https"); 189 | let res = flags.push("--http"); 190 | assert!(res.is_err()); 191 | assert_eq!(res.unwrap_err(), Error::ContradictoryScheme); 192 | } 193 | 194 | #[test] 195 | fn compact_flags() { 196 | let flags = flag!["-hH"]; 197 | assert_eq!(flags.show_request_headers, true); 198 | assert_eq!(flags.show_response_headers, true); 199 | 200 | let flags = flag!["-Hh"]; 201 | assert_eq!(flags.show_request_headers, true); 202 | assert_eq!(flags.show_response_headers, true); 203 | 204 | let flag = "-hHa"; 205 | let mut flags = flag![]; 206 | let res = flags.push(flag); 207 | assert!(res.is_err()); 208 | assert_eq!(res.unwrap_err(), Error::InvalidFlag(flag.into())); 209 | 210 | let flag = "-ahH"; 211 | let mut flags = flag![]; 212 | let res = flags.push(flag); 213 | assert!(res.is_err()); 214 | assert_eq!(res.unwrap_err(), Error::InvalidFlag(flag.into())); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /crates/cli/src/parser/headers.rs: -------------------------------------------------------------------------------- 1 | pub use crate::core::HeaderMap; 2 | use crate::core::{Error, PushDataItem, Result}; 3 | use reqwest::header::{HeaderName, HeaderValue}; 4 | use std::str::FromStr; 5 | 6 | impl PushDataItem for HeaderMap { 7 | fn push(&mut self, item: &str) -> Result<()> { 8 | match item.split_once(":") { 9 | Some(parts) => { 10 | let key = HeaderName::from_str(parts.0)?; 11 | let value = HeaderValue::from_str(parts.1)?; 12 | self.append(key, value); 13 | } 14 | None => return Err(Error::InvalidHeader(item.into())), 15 | }; 16 | Ok(()) 17 | } 18 | } 19 | 20 | impl From for Error { 21 | fn from(err: reqwest::header::InvalidHeaderName) -> Error { 22 | Error::BadHeaderName(err.to_string()) // FIXME The err doesn't contain the header name 23 | } 24 | } 25 | 26 | impl From for Error { 27 | fn from(err: reqwest::header::InvalidHeaderValue) -> Error { 28 | Error::BadHeaderValue(err.to_string()) // FIXME The err doesn't contain the header value 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /crates/cli/src/parser/method.rs: -------------------------------------------------------------------------------- 1 | use crate::request::Method; 2 | 3 | pub fn from_str(keyword: &str) -> Option { 4 | if !is_valid(keyword) { 5 | return None; 6 | } 7 | 8 | let method = reqwest::Method::from_bytes(keyword.as_bytes()); 9 | match method { 10 | Ok(m) => Some(m), 11 | _ => None, 12 | } 13 | } 14 | 15 | fn is_valid(keyword: &str) -> bool { 16 | for c in keyword.chars() { 17 | if !c.is_uppercase() { 18 | return false; 19 | } 20 | } 21 | true 22 | } 23 | 24 | // fn is_standard(keyword: &str) -> bool { 25 | // let length = keyword.len(); 26 | // if length != 3 && length != 4 && length != 5 && length != 6 && length != 7 { 27 | // return false; 28 | // } 29 | // [ 30 | // "GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS", "CONNECT", "PATCH", "TRACE", 31 | // ] 32 | // .contains(&keyword) 33 | // } 34 | 35 | // UNIT TESTS ///////////////////////////////////////////////////////////////////////////// 36 | 37 | #[cfg(test)] 38 | mod tests { 39 | use super::{from_str, Method}; 40 | 41 | macro_rules! assert_standard_method_eq { 42 | ($method:expr, $expected:expr) => { 43 | assert_eq!(from_str($method).unwrap(), $expected) 44 | }; 45 | } 46 | 47 | macro_rules! assert_standard_method_invalid { 48 | ($method:expr) => { 49 | assert!(from_str($method).is_none()) 50 | }; 51 | } 52 | 53 | #[test] 54 | fn standard() { 55 | assert_standard_method_eq!("GET", Method::GET); 56 | assert_standard_method_eq!("POST", Method::POST); 57 | assert_standard_method_eq!("PUT", Method::PUT); 58 | assert_standard_method_eq!("DELETE", Method::DELETE); 59 | assert_standard_method_eq!("HEAD", Method::HEAD); 60 | assert_standard_method_eq!("OPTIONS", Method::OPTIONS); 61 | assert_standard_method_eq!("CONNECT", Method::CONNECT); 62 | assert_standard_method_eq!("PATCH", Method::PATCH); 63 | assert_standard_method_eq!("TRACE", Method::TRACE); 64 | } 65 | 66 | #[test] 67 | fn custom() { 68 | assert_standard_method_eq!("HELLO", Method::from_bytes(b"HELLO").unwrap()); 69 | assert_standard_method_eq!("WORLD", Method::from_bytes(b"WORLD").unwrap()); 70 | } 71 | 72 | #[test] 73 | fn invalid() { 74 | assert_standard_method_invalid!("test"); 75 | 76 | assert_standard_method_invalid!("get"); 77 | assert_standard_method_invalid!("post"); 78 | assert_standard_method_invalid!("put"); 79 | assert_standard_method_invalid!("delete"); 80 | assert_standard_method_invalid!("head"); 81 | assert_standard_method_invalid!("options"); 82 | assert_standard_method_invalid!("connect"); 83 | assert_standard_method_invalid!("patch"); 84 | assert_standard_method_invalid!("trace"); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /crates/cli/src/parser/mod.rs: -------------------------------------------------------------------------------- 1 | mod core; 2 | mod error; 3 | mod flags; 4 | mod headers; 5 | mod method; 6 | mod normalizer; 7 | mod url; 8 | 9 | use crate::core::{Error, Flags, Result, Workspace}; 10 | use crate::items::Items; 11 | use crate::shell::stream; 12 | use crate::theme::default::DefaultTheme; 13 | use normalizer::Normalizer; 14 | use std::cell::RefCell; 15 | use std::io::{self, Read}; 16 | 17 | pub fn execute(args: &[String]) -> Result { 18 | validate_there_are_enough_args(args)?; 19 | 20 | let output_redirected = !stream::is_stdout(); 21 | let mut normalizer = Normalizer::parse(args, output_redirected, "http", "localhost")?; 22 | let method = normalizer.method(); 23 | let flags = normalizer.flags; 24 | let headers = normalizer.headers; 25 | let items = normalizer.items; 26 | let urls = normalizer.urls; 27 | let mut raw = normalizer.raw.take(); 28 | let certificate_authority_file = normalizer.certificate_authority_file.take(); 29 | 30 | let input_redirected = !stream::is_stdin(); 31 | if !is_flag_only_command(&flags) { 32 | validate_processed_urls(&urls, &flags, args)?; 33 | validate_there_is_no_mix_of_items_and_raw_and_stdin(&items, &raw, input_redirected)?; 34 | } 35 | 36 | if input_redirected { 37 | extract_input_as_raw_data(&mut raw)?; 38 | } 39 | 40 | Ok(Workspace { 41 | method, 42 | urls, 43 | output_redirected, 44 | terminal_columns: terminal_columns(), 45 | theme: Box::new(DefaultTheme::new()), 46 | flags, 47 | headers: RefCell::new(headers), 48 | items: RefCell::new(items), 49 | raw, 50 | certificate_authority_file, 51 | }) 52 | } 53 | 54 | fn extract_input_as_raw_data(raw: &mut Option) -> Result<()> { 55 | let mut buffer = String::new(); 56 | io::stdin().read_to_string(&mut buffer)?; 57 | *raw = Some(buffer); 58 | Ok(()) 59 | } 60 | 61 | #[inline] 62 | fn validate_there_are_enough_args(args: &[String]) -> Result<()> { 63 | let count = args.len(); 64 | if count == 0 { 65 | Err(Error::NoArgs) 66 | } else { 67 | Ok(()) 68 | } 69 | } 70 | 71 | #[inline] 72 | fn validate_processed_urls(urls: &[String], flags: &Flags, args: &[String]) -> Result<()> { 73 | if urls.is_empty() { 74 | if short_help_flag(flags, args) { 75 | Ok(()) 76 | } else { 77 | Err(Error::MissingUrl) 78 | } 79 | } else { 80 | Ok(()) 81 | } 82 | } 83 | 84 | #[inline] 85 | fn validate_there_is_no_mix_of_items_and_raw_and_stdin(items: &Items, raw: &Option, input_redirected: bool) -> Result<()> { 86 | if (!items.is_empty()) as u8 + raw.is_some() as u8 + input_redirected as u8 > 1 { 87 | Err(Error::ItemsAndRawMix) 88 | } else { 89 | Ok(()) 90 | } 91 | } 92 | 93 | #[inline] 94 | fn is_flag_only_command(flags: &Flags) -> bool { 95 | flags.show_version || flags.show_help || flags.debug 96 | } 97 | 98 | #[inline] 99 | fn short_help_flag(flags: &Flags, args: &[String]) -> bool { 100 | flags.show_short_help && args.len() == 1 101 | } 102 | 103 | fn terminal_columns() -> u16 { 104 | match termsize::get() { 105 | Some(size) => size.cols, 106 | None => 100, 107 | } 108 | } 109 | 110 | // UNIT TESTS ///////////////////////////////////////////////////////////////////////////// 111 | 112 | #[cfg(test)] 113 | mod tests { 114 | use super::*; 115 | 116 | mod basic { 117 | use super::*; 118 | 119 | #[test] 120 | fn show_version() { 121 | let args = rh_test::args!["--version"]; 122 | let parser = execute(&args).unwrap(); 123 | assert_eq!(parser.flags.show_version, true); 124 | } 125 | 126 | // #[test] 127 | // fn show_short_version() { 128 | // let args = rh_test::args!["-v"]; 129 | // let parser = execute(&args).unwrap(); 130 | // assert_eq!(parser.flags.show_short_version, true); 131 | // } 132 | 133 | #[test] 134 | fn show_help() { 135 | let args = rh_test::args!["--help"]; 136 | let parser = execute(&args).unwrap(); 137 | assert_eq!(parser.flags.show_help, true); 138 | } 139 | 140 | #[test] 141 | fn show_short_help() { 142 | let args = rh_test::args!["-h"]; 143 | let parser = execute(&args).unwrap(); 144 | assert_eq!(parser.flags.show_short_help, true); 145 | } 146 | } 147 | 148 | mod validate { 149 | use super::*; 150 | use crate::core::PushDataItem; 151 | 152 | const NO_STDIN_DATA: bool = false; 153 | const STDIN_DATA: bool = true; 154 | 155 | #[test] 156 | fn flag_only_commands() { 157 | let mut flags = Flags::default(); 158 | flags.show_help = true; 159 | assert!(is_flag_only_command(&flags)); 160 | 161 | let mut flags = Flags::default(); 162 | flags.show_version = true; 163 | assert!(is_flag_only_command(&flags)); 164 | } 165 | 166 | #[test] 167 | fn error_if_no_args() { 168 | let args = rh_test::args![]; 169 | let parser = validate_there_are_enough_args(&args); 170 | assert!(parser.is_err()); 171 | assert_eq!(parser.unwrap_err(), Error::NoArgs); 172 | } 173 | 174 | #[test] 175 | fn basic_validation_if_multi_args() { 176 | let args = rh_test::args!["GET", "localhost"]; 177 | let parser = validate_there_are_enough_args(&args); 178 | assert!(parser.is_ok()); 179 | } 180 | 181 | #[test] 182 | fn error_if_no_urls() { 183 | let args = rh_test::args![]; 184 | let flags = Flags::default(); 185 | let parser = validate_processed_urls(&[], &flags, &args); 186 | assert!(parser.is_err()); 187 | assert_eq!(parser.unwrap_err(), Error::MissingUrl); 188 | } 189 | 190 | #[test] 191 | fn validate_if_one_url() { 192 | let args = rh_test::args!["test.com"]; 193 | let flags = Flags::default(); 194 | let urls = rh_test::args!["test.com"]; 195 | let parser = validate_processed_urls(&urls, &flags, &args); 196 | assert!(parser.is_ok()); 197 | } 198 | 199 | #[test] 200 | fn raw_data_only() { 201 | let items = Items::new(); 202 | let raw_data = Some("hello".into()); 203 | let parser = validate_there_is_no_mix_of_items_and_raw_and_stdin(&items, &raw_data, NO_STDIN_DATA); 204 | assert!(parser.is_ok()); 205 | } 206 | 207 | #[test] 208 | fn key_value_only() { 209 | let mut items = Items::new(); 210 | let _ = items.push("key=value"); 211 | let parser = validate_there_is_no_mix_of_items_and_raw_and_stdin(&items, &None, NO_STDIN_DATA); 212 | assert!(parser.is_ok()); 213 | } 214 | 215 | #[test] 216 | fn stdin_only() { 217 | let items = Items::new(); 218 | let parser = validate_there_is_no_mix_of_items_and_raw_and_stdin(&items, &None, STDIN_DATA); 219 | assert!(parser.is_ok()); 220 | } 221 | 222 | #[test] 223 | fn error_if_mix_raw_and_stdin() { 224 | let items = Items::new(); 225 | let raw_data = Some("hello".into()); 226 | let parser = validate_there_is_no_mix_of_items_and_raw_and_stdin(&items, &raw_data, STDIN_DATA); 227 | assert!(parser.is_err()); 228 | assert_eq!(parser.unwrap_err(), Error::ItemsAndRawMix); 229 | } 230 | 231 | #[test] 232 | fn error_if_mix_key_value_and_raw() { 233 | let mut items = Items::new(); 234 | items.push("key=value").expect("Cannot add key/value item"); 235 | let raw_data = Some("hello".into()); 236 | let parser = validate_there_is_no_mix_of_items_and_raw_and_stdin(&items, &raw_data, NO_STDIN_DATA); 237 | assert!(parser.is_err()); 238 | assert_eq!(parser.unwrap_err(), Error::ItemsAndRawMix); 239 | } 240 | 241 | #[test] 242 | fn error_if_mix_key_value_and_stdin() { 243 | let mut items = Items::new(); 244 | items.push("key=value").expect("Cannot add key/value item"); 245 | let raw_data = Some("hello".into()); 246 | let parser = validate_there_is_no_mix_of_items_and_raw_and_stdin(&items, &raw_data, STDIN_DATA); 247 | assert!(parser.is_err()); 248 | assert_eq!(parser.unwrap_err(), Error::ItemsAndRawMix); 249 | } 250 | } 251 | 252 | mod urls { 253 | use super::*; 254 | 255 | #[test] 256 | fn hostname_only() { 257 | let args = rh_test::args!["localhost"]; 258 | let parser = execute(&args).unwrap(); 259 | assert_eq!(parser.urls.len(), 1); 260 | rh_test::assert_str_eq!(parser.urls[0], "http://localhost"); 261 | } 262 | 263 | #[test] 264 | fn method_and_hostname() { 265 | let args = rh_test::args!["GET", "localhost"]; 266 | let parser = execute(&args).unwrap(); 267 | assert_eq!(parser.urls.len(), 1); 268 | rh_test::assert_str_eq!(parser.urls[0], "http://localhost"); 269 | } 270 | 271 | #[test] 272 | fn method_and_hostname_and_flag() { 273 | let args = rh_test::args!["GET", "localhost", "--headers"]; 274 | let parser = execute(&args).unwrap(); 275 | assert_eq!(parser.urls.len(), 1); 276 | rh_test::assert_str_eq!(parser.urls[0], "http://localhost"); 277 | assert_eq!(parser.flags.show_request_headers, true); 278 | assert_eq!(parser.flags.show_response_headers, true); 279 | } 280 | 281 | #[test] 282 | fn detect_obvious_url() { 283 | let args = rh_test::args!["GET", "--url", "http://test.com", "--headers"]; 284 | let parser = execute(&args).unwrap(); 285 | assert_eq!(parser.urls.len(), 1); 286 | rh_test::assert_str_eq!(parser.urls[0], "http://test.com"); 287 | assert_eq!(parser.flags.show_request_url, true); 288 | assert_eq!(parser.flags.show_request_headers, true); 289 | assert_eq!(parser.flags.show_response_headers, true); 290 | } 291 | 292 | #[test] 293 | fn error_if_multi_args_including_method_but_method_at_wrong_place() { 294 | let args = rh_test::args!["GET", "--url", "--headers", "https://test.com"]; 295 | let parser = execute(&args).unwrap(); 296 | assert_eq!(parser.urls.len(), 1); 297 | rh_test::assert_str_eq!(parser.urls[0], "https://test.com"); 298 | assert_eq!(parser.flags.show_request_url, true); 299 | assert_eq!(parser.flags.show_request_headers, true); 300 | assert_eq!(parser.flags.show_response_headers, true); 301 | } 302 | 303 | #[test] 304 | fn error_if_one_arg_but_no_url() { 305 | let args: Vec = rh_test::args!["--url"]; 306 | let parser = execute(&args); 307 | assert!(parser.is_err()); 308 | assert_eq!(parser.unwrap_err(), Error::MissingUrl); 309 | } 310 | 311 | #[test] 312 | fn error_if_multi_args_but_no_url() { 313 | let args = rh_test::args!["--url", "--headers"]; 314 | let parser = execute(&args); 315 | assert!(parser.is_err()); 316 | assert_eq!(parser.unwrap_err(), Error::MissingUrl); 317 | } 318 | 319 | #[test] 320 | fn error_if_multi_args_including_method_but_no_url() { 321 | let args = rh_test::args!["GET", "--url", "--headers"]; 322 | let parser = execute(&args); 323 | assert!(parser.is_err()); 324 | assert_eq!(parser.unwrap_err(), Error::MissingUrl); 325 | } 326 | } 327 | 328 | mod raw { 329 | use super::*; 330 | 331 | #[test] 332 | fn error_if_raw_data_and_json() { 333 | let args: Vec = rh_test::args!["test.com", "--raw=data", "key=value"]; 334 | let parser = execute(&args); 335 | assert!(parser.is_err()); 336 | assert_eq!(parser.unwrap_err(), Error::ItemsAndRawMix); 337 | } 338 | } 339 | } 340 | -------------------------------------------------------------------------------- /crates/cli/src/parser/normalizer.rs: -------------------------------------------------------------------------------- 1 | use super::core::{ArgDetection, CAFILE_FLAG, RAW_FLAG}; 2 | use super::headers::HeaderMap; 3 | use super::method; 4 | use super::url; 5 | use crate::core::Flags; 6 | use crate::core::{Error, PushDataItem}; 7 | use crate::items::Items; 8 | use crate::request::Method; 9 | 10 | #[cfg_attr(test, derive(Debug))] 11 | pub struct Normalizer { 12 | pub urls: Vec, 13 | method: Option, 14 | pub flags: Flags, 15 | pub headers: HeaderMap, 16 | pub items: Items, 17 | pub raw: Option, 18 | pub certificate_authority_file: Option, 19 | } 20 | 21 | impl Normalizer { 22 | pub fn parse(args: &[String], output_redirected: bool, default_scheme: &str, default_host: &str) -> Result { 23 | let mut method: Option = None; 24 | let mut urls: Vec = Vec::new(); 25 | let mut flags = Flags::new(output_redirected); 26 | let mut headers = HeaderMap::new(); 27 | let mut items = Items::new(); 28 | let mut raw: Option = None; 29 | let mut certificate_authority_file: Option = None; 30 | let args_length = args.len(); 31 | 32 | for (arg_index, arg) in args.iter().enumerate().take(args_length) { 33 | if arg_index == 0 { 34 | method = method::from_str(arg); 35 | if method.is_some() { 36 | continue; 37 | } 38 | } 39 | 40 | if (method.is_some() && arg_index == 1) || (method.is_none() && arg_index == 0) { 41 | if arg.is_likely_url() { 42 | urls.push(arg.clone()); 43 | continue; 44 | } 45 | } else if arg.is_url() || arg.is_very_likely_url() { 46 | urls.push(arg.clone()); 47 | continue; 48 | } 49 | 50 | if arg.is_raw_flag() { 51 | let raw_data = arg[RAW_FLAG.len()..].to_string(); 52 | if raw.is_some() { 53 | return Err(Error::TooManyRaw); 54 | } 55 | if !raw_data.is_empty() { 56 | raw = Some(raw_data); 57 | } 58 | } else if arg.is_cafile_flag() { 59 | let cafile = arg[CAFILE_FLAG.len()..].to_string(); 60 | if !cafile.is_empty() { 61 | certificate_authority_file = Some(cafile); 62 | } 63 | } else if arg.is_flag() { 64 | flags.push(arg)?; 65 | } else if arg.is_header() { 66 | headers.push(arg)?; 67 | } else if arg.is_item() { 68 | items.push(arg)?; 69 | } else if method.is_none() { 70 | return Err(Error::Unexpected(arg.clone())); 71 | } 72 | 73 | if flags.show_version || flags.show_help { 74 | break; 75 | } 76 | } 77 | 78 | if !flags.http && !flags.https { 79 | flags.http = true; 80 | } 81 | 82 | if !urls.is_empty() { 83 | let scheme = if flags.https { 84 | "https" 85 | } else if flags.http { 86 | "http" 87 | } else { 88 | default_scheme 89 | }; 90 | for url in urls.iter_mut() { 91 | *url = url::normalize(url, scheme, default_host); 92 | } 93 | } 94 | 95 | Ok(Normalizer { 96 | urls, 97 | method, 98 | flags, 99 | headers, 100 | items, 101 | raw, 102 | certificate_authority_file, 103 | }) 104 | } 105 | 106 | pub fn method(&mut self) -> Method { 107 | let method = self.method.take(); 108 | match method { 109 | Some(method) => method, 110 | _ => { 111 | if self.has_input_data() { 112 | Method::POST 113 | } else { 114 | Method::GET 115 | } 116 | } 117 | } 118 | } 119 | 120 | pub fn has_input_data(&self) -> bool { 121 | !self.items.is_empty() || self.raw.is_some() 122 | } 123 | } 124 | 125 | // UNIT TESTS ///////////////////////////////////////////////////////////////////////////// 126 | 127 | // FIXME More tests (in particular if output_redirected=true) 128 | #[cfg(test)] 129 | mod tests { 130 | use super::{Error, Normalizer}; 131 | const DEFAULT_SCHEME: &str = "http"; 132 | const DEFAULT_HOST: &str = "l-o-c-a-l-h-o-s-t"; 133 | 134 | macro_rules! assert_one_arg_url_eq { 135 | ($url:expr, $expected:expr) => { 136 | let args: Vec = rh_test::args![$url]; 137 | let mut normalizer = Normalizer::parse(&args, false, DEFAULT_SCHEME, DEFAULT_HOST).unwrap(); 138 | assert!(normalizer.method() == Method::GET); 139 | assert_eq!(normalizer.urls.len(), 1); 140 | rh_test::assert_str_eq!(normalizer.urls[0], $expected); 141 | }; 142 | } 143 | 144 | mod method { 145 | use super::*; 146 | use super::{DEFAULT_HOST, DEFAULT_SCHEME}; 147 | use crate::request::Method; 148 | 149 | #[test] 150 | fn standard_method() { 151 | let args = rh_test::args!["HEAD", "localhost"]; 152 | let normalizer = Normalizer::parse(&args, false, DEFAULT_SCHEME, DEFAULT_HOST).expect("Cannot parse standard method"); 153 | assert_eq!(normalizer.method, Some(Method::HEAD)); 154 | assert_eq!(normalizer.urls.len(), 1); 155 | } 156 | 157 | #[test] 158 | fn custom_method() { 159 | let args = rh_test::args!["HELLO", "localhost"]; 160 | let normalizer = Normalizer::parse(&args, false, DEFAULT_SCHEME, DEFAULT_HOST).expect("Cannot parse custom method"); 161 | assert_eq!(normalizer.method, Some(Method::from_bytes(b"HELLO").unwrap())); 162 | assert_eq!(normalizer.urls.len(), 1); 163 | } 164 | 165 | #[test] 166 | fn no_methods_because_lowercase() { 167 | let args = rh_test::args!["get", "localhost"]; 168 | let normalizer = Normalizer::parse(&args, false, DEFAULT_SCHEME, DEFAULT_HOST).expect("Cannot parse multi-urls"); 169 | assert_eq!(normalizer.urls.len(), 2); 170 | } 171 | } 172 | 173 | mod urls { 174 | use super::{Error, Normalizer}; 175 | use super::{DEFAULT_HOST, DEFAULT_SCHEME}; 176 | use crate::request::Method; 177 | 178 | #[test] 179 | fn no_args() { 180 | let args = rh_test::args![]; 181 | let normalizer = Normalizer::parse(&args, false, DEFAULT_SCHEME, DEFAULT_HOST).unwrap(); 182 | assert_eq!(normalizer.method, None); 183 | assert_eq!(normalizer.urls.len(), 0); 184 | } 185 | 186 | #[test] 187 | fn only_one_url_arg() { 188 | assert_one_arg_url_eq!("http://test.com", "http://test.com"); 189 | assert_one_arg_url_eq!("test.com", &format!("{}://test.com", DEFAULT_SCHEME)); 190 | assert_one_arg_url_eq!("test", &format!("{}://test", DEFAULT_SCHEME)); 191 | } 192 | 193 | #[test] 194 | fn method_and_url() -> Result<(), Error> { 195 | let args = rh_test::args!["GET", "localhost"]; 196 | let normalizer = Normalizer::parse(&args, false, DEFAULT_SCHEME, DEFAULT_HOST)?; 197 | assert_eq!(normalizer.urls.len(), 1); 198 | rh_test::assert_str_eq!(normalizer.urls[0], format!("{}://localhost", DEFAULT_SCHEME)); 199 | Ok(()) 200 | } 201 | 202 | #[test] 203 | fn method_and_url_and_flag() -> Result<(), Error> { 204 | let args = rh_test::args!["GET", "localhost", "--headers"]; 205 | let normalizer = Normalizer::parse(&args, false, DEFAULT_SCHEME, DEFAULT_HOST)?; 206 | assert_eq!(normalizer.urls.len(), 1); 207 | rh_test::assert_str_eq!(normalizer.urls[0], format!("{}://localhost", DEFAULT_SCHEME)); 208 | Ok(()) 209 | } 210 | 211 | #[test] 212 | fn url_and_flag() -> Result<(), Error> { 213 | let args = rh_test::args!["localhost", "--headers"]; 214 | let normalizer = Normalizer::parse(&args, false, DEFAULT_SCHEME, DEFAULT_HOST)?; 215 | assert_eq!(normalizer.urls.len(), 1); 216 | rh_test::assert_str_eq!(normalizer.urls[0], format!("{}://localhost", DEFAULT_SCHEME)); 217 | Ok(()) 218 | } 219 | } 220 | 221 | mod flags { 222 | use super::{Error, Normalizer}; 223 | use super::{DEFAULT_HOST, DEFAULT_SCHEME}; 224 | use crate::request::Method; 225 | 226 | #[test] 227 | fn force_http() -> Result<(), Error> { 228 | let args: Vec = rh_test::args!["GET", "test.com", "--http"]; 229 | let mut normalizer = Normalizer::parse(&args, false, DEFAULT_SCHEME, DEFAULT_HOST)?; 230 | assert!(normalizer.method() == Method::GET); 231 | assert_eq!(normalizer.urls.len(), 1); 232 | rh_test::assert_str_eq!(normalizer.urls[0], "http://test.com"); 233 | assert_eq!(normalizer.flags.http, true); 234 | assert_eq!(normalizer.flags.https, false); 235 | Ok(()) 236 | } 237 | 238 | #[test] 239 | fn force_https() -> Result<(), Error> { 240 | let args: Vec = rh_test::args!["GET", "test.com", "--https"]; 241 | let mut normalizer = Normalizer::parse(&args, false, DEFAULT_SCHEME, DEFAULT_HOST)?; 242 | assert!(normalizer.method() == Method::GET); 243 | assert_eq!(normalizer.urls.len(), 1); 244 | rh_test::assert_str_eq!(normalizer.urls[0], "https://test.com"); 245 | assert_eq!(normalizer.flags.http, false); 246 | assert_eq!(normalizer.flags.https, true); 247 | Ok(()) 248 | } 249 | 250 | #[test] 251 | fn version() -> Result<(), Error> { 252 | let args: Vec = rh_test::args!["--version"]; 253 | let normalizer = Normalizer::parse(&args, false, DEFAULT_SCHEME, DEFAULT_HOST)?; 254 | assert_eq!(normalizer.urls.len(), 0); 255 | assert_eq!(normalizer.method, None); 256 | assert_eq!(normalizer.flags.show_version, true); 257 | Ok(()) 258 | } 259 | 260 | #[test] 261 | fn help() -> Result<(), Error> { 262 | let args: Vec = rh_test::args!["--help"]; 263 | let normalizer = Normalizer::parse(&args, false, DEFAULT_SCHEME, DEFAULT_HOST)?; 264 | assert_eq!(normalizer.urls.len(), 0); 265 | assert_eq!(normalizer.method, None); 266 | assert_eq!(normalizer.flags.show_help, true); 267 | Ok(()) 268 | } 269 | } 270 | 271 | mod raw { 272 | use super::Normalizer; 273 | use super::{DEFAULT_HOST, DEFAULT_SCHEME}; 274 | use crate::request::Method; 275 | 276 | #[test] 277 | fn raw_data() { 278 | let args: Vec = rh_test::args!["test.com", "--raw=~data~"]; 279 | let mut normalizer = Normalizer::parse(&args, false, DEFAULT_SCHEME, DEFAULT_HOST).unwrap(); 280 | assert_eq!(normalizer.method(), Method::POST); 281 | assert_eq!(normalizer.urls.len(), 1); 282 | rh_test::assert_str_eq!(normalizer.urls[0], format!("{}://test.com", DEFAULT_SCHEME)); 283 | assert_eq!(normalizer.raw, Some("~data~".to_string())); 284 | assert_eq!(normalizer.flags.as_json, false); 285 | assert_eq!(normalizer.flags.as_form, false); 286 | } 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /crates/cli/src/parser/url.rs: -------------------------------------------------------------------------------- 1 | use regex::Regex; 2 | use url::Url; 3 | 4 | pub fn normalize(url: &str, default_scheme: &str, default_host: &str) -> String { 5 | let res = Url::parse(url); 6 | if res.is_ok() { 7 | if url.starts_with("http") { 8 | return url.into(); 9 | } else { 10 | return format!("{}://{}", default_scheme, url); 11 | } 12 | } 13 | match url { 14 | ":" => format!("{}://{}", default_scheme, default_host), 15 | part if Regex::new(r"^://").unwrap().is_match(part) => { 16 | format!("{}://{}{}", default_scheme, default_host, &part[2..]) 17 | } 18 | part if Regex::new(r"^:/").unwrap().is_match(part) => { 19 | format!("{}://{}{}", default_scheme, default_host, &part[1..]) 20 | } 21 | part if Regex::new(r"^:\d").unwrap().is_match(part) => { 22 | format!("{}://{}{}", default_scheme, default_host, part) 23 | } 24 | part if Regex::new(r"^/").unwrap().is_match(part) => { 25 | format!("{}://{}{}", default_scheme, default_host, part) 26 | } 27 | _ => { 28 | if url.starts_with('/') { 29 | format!("{}://{}/{}", default_scheme, default_host, url) 30 | } else { 31 | format!("{}://{}", default_scheme, url) 32 | } 33 | } 34 | } 35 | } 36 | 37 | // UNIT TESTS ///////////////////////////////////////////////////////////////////////////// 38 | 39 | #[cfg(test)] 40 | mod tests { 41 | use super::normalize; 42 | const DEFAULT_SCHEME: &str = "https"; 43 | const DEFAULT_HOST: &str = "l-o-c-a-l-h-o-s-t"; 44 | 45 | macro_rules! assert_normalize { 46 | ($url:expr, $expected:expr) => { 47 | assert_eq!(normalize($url, DEFAULT_SCHEME, DEFAULT_HOST), $expected) 48 | }; 49 | } 50 | 51 | macro_rules! assert_normalize_with_defaults { 52 | ($url:expr, $expected:expr) => { 53 | assert_eq!(normalize($url, DEFAULT_SCHEME, DEFAULT_HOST), format!($expected, DEFAULT_SCHEME, DEFAULT_HOST)) 54 | }; 55 | } 56 | 57 | macro_rules! assert_normalize_with_default_scheme { 58 | ($url:expr, $expected:expr) => { 59 | assert_eq!(normalize($url, DEFAULT_SCHEME, DEFAULT_HOST), format!($expected, DEFAULT_SCHEME)) 60 | }; 61 | } 62 | 63 | macro_rules! assert_valid { 64 | ($url:expr) => { 65 | assert_eq!(normalize($url, DEFAULT_SCHEME, DEFAULT_HOST), $url) 66 | }; 67 | } 68 | 69 | #[test] 70 | fn macro_assert_normalise() { 71 | assert_valid!("http://test.com"); 72 | assert_normalize!("http://test.com", "http://test.com"); 73 | assert_normalize_with_defaults!(format!("{}://{}", DEFAULT_SCHEME, DEFAULT_HOST).as_str(), "{}://{}"); 74 | assert_normalize_with_default_scheme!(format!("{}://{}", DEFAULT_SCHEME, "host-example.com").as_str(), "{}://host-example.com"); 75 | } 76 | 77 | #[test] 78 | fn host() { 79 | assert_normalize_with_default_scheme!("localhost", "{}://localhost"); 80 | assert_normalize_with_default_scheme!("localhost:9200", "{}://localhost:9200"); 81 | assert_normalize_with_default_scheme!("test.com", "{}://test.com"); 82 | assert_normalize_with_default_scheme!("test.com:9200", "{}://test.com:9200"); 83 | assert_normalize_with_default_scheme!("test.com/a?b=c", "{}://test.com/a?b=c"); 84 | assert_normalize_with_default_scheme!("test.com:1024/a?b=c", "{}://test.com:1024/a?b=c"); 85 | assert_normalize_with_default_scheme!("test.com/a/b/c", "{}://test.com/a/b/c"); 86 | assert_normalize_with_default_scheme!("test.com:1024/a/b/c", "{}://test.com:1024/a/b/c"); 87 | } 88 | 89 | #[test] 90 | fn default_host() { 91 | assert_normalize_with_defaults!(":", "{}://{}"); 92 | assert_normalize_with_defaults!(":/", "{}://{}/"); 93 | assert_normalize_with_defaults!(":/uri", "{}://{}/uri"); 94 | assert_normalize_with_defaults!("://uri", "{}://{}/uri"); 95 | assert_normalize_with_defaults!(":/uri/a/b/c", "{}://{}/uri/a/b/c"); 96 | assert_normalize_with_defaults!(":/uri/a/b/c/d.html", "{}://{}/uri/a/b/c/d.html"); 97 | assert_normalize_with_defaults!(":9000", "{}://{}:9000"); 98 | assert_normalize_with_defaults!(":5000/", "{}://{}:5000/"); 99 | assert_normalize_with_defaults!(":2000/uri", "{}://{}:2000/uri"); 100 | assert_normalize_with_defaults!("/uri", "{}://{}/uri"); 101 | assert_normalize_with_defaults!("/uri/a.jpeg", "{}://{}/uri/a.jpeg"); 102 | assert_normalize_with_defaults!(DEFAULT_HOST, "{}://{}"); 103 | } 104 | 105 | #[test] 106 | fn proper_urls() { 107 | assert_valid!("http://test.com"); 108 | assert_valid!("https://test.com"); 109 | assert_valid!("http://test.com:9000"); 110 | assert_valid!("https://test.com:9000"); 111 | assert_valid!("https://test.com:9000/a/b.html"); 112 | assert_valid!("https://test.com:9000/a/b/"); 113 | assert_valid!("https://test.com:9000/a/b.html?c=d"); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /crates/cli/src/request/body/form.rs: -------------------------------------------------------------------------------- 1 | use serde_urlencoded::ser::Error; 2 | 3 | use crate::items::Items; 4 | 5 | pub fn serialize(items: &Items) -> Result { 6 | serde_urlencoded::to_string(&items) 7 | } 8 | -------------------------------------------------------------------------------- /crates/cli/src/request/body/json.rs: -------------------------------------------------------------------------------- 1 | use serde_json::error::Error; 2 | 3 | use crate::items::Items; 4 | 5 | pub fn serialize(items: &Items) -> Result { 6 | serde_json::to_string(&items) 7 | } 8 | -------------------------------------------------------------------------------- /crates/cli/src/request/body/mod.rs: -------------------------------------------------------------------------------- 1 | mod form; 2 | mod json; 3 | 4 | use crate::core::{Workspace, WorkspaceData}; 5 | use reqwest::blocking::RequestBuilder; 6 | 7 | pub trait Body { 8 | fn body_if_items(self, args: &Workspace) -> RequestBuilder; 9 | } 10 | 11 | impl Body for RequestBuilder { 12 | fn body_if_items(self, args: &Workspace) -> RequestBuilder { 13 | match build_body(args) { 14 | Some(body) => self.body(body), 15 | None => self, 16 | } 17 | } 18 | } 19 | 20 | fn build_body(args: &Workspace) -> Option { 21 | if args.has_items() { 22 | if args.is_json() { 23 | Some(json::serialize(&args.items.borrow()).unwrap()) 24 | } else { 25 | Some(form::serialize(&args.items.borrow()).unwrap()) 26 | } 27 | } else { 28 | args.raw.as_ref().cloned() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /crates/cli/src/request/certificate.rs: -------------------------------------------------------------------------------- 1 | use reqwest::Certificate; 2 | 3 | use crate::core::Result; 4 | use std::io::Read; 5 | use std::{fs::File, path::Path}; 6 | 7 | pub fn load>(path: P) -> Result { 8 | let mut buf = Vec::new(); 9 | File::open(&path)?.read_to_end(&mut buf)?; 10 | let cert = certificate(path, &buf)?; 11 | Ok(cert) 12 | } 13 | 14 | fn certificate>(path: P, buf: &[u8]) -> Result { 15 | let cert = if Some(std::ffi::OsStr::new("der")) == path.as_ref().extension() { 16 | Certificate::from_der(buf) 17 | } else { 18 | Certificate::from_pem(buf) 19 | }?; 20 | Ok(cert) 21 | } 22 | -------------------------------------------------------------------------------- /crates/cli/src/request/header.rs: -------------------------------------------------------------------------------- 1 | pub const ACCEPT: &str = "accept"; 2 | pub const CONTENT_TYPE: &str = "content-type"; 3 | pub const USER_AGENT: &str = "user-agent"; 4 | 5 | pub trait StandardHeader { 6 | fn is_standard(&self) -> bool; 7 | } 8 | 9 | impl StandardHeader for reqwest::header::HeaderName { 10 | fn is_standard(&self) -> bool { 11 | let header_name = self.as_str(); 12 | [ 13 | ACCEPT, 14 | "accept-ch", 15 | "accept-ch-lifetime", 16 | "accept-encoding", 17 | "accept-language", 18 | "accept-push-policy", 19 | "accept-ranges", 20 | "accept-signature", 21 | "access-control-allow-credentials", 22 | "access-control-allow-headers", 23 | "access-control-allow-methods", 24 | "access-control-allow-origin", 25 | "access-control-expose-headers", 26 | "access-control-max-age", 27 | "access-control-request-headers", 28 | "access-control-request-method", 29 | "age", 30 | "allow", 31 | "alt-svc", 32 | "authorization", 33 | "cache-control", 34 | "clear-site-data", 35 | "connection", 36 | "content-disposition", 37 | "content-dpr", 38 | "content-encoding", 39 | "content-language", 40 | "content-length", 41 | "content-location", 42 | "content-range", 43 | "content-security-policy", 44 | "content-security-policy-report-only", 45 | CONTENT_TYPE, 46 | "cookie", 47 | "cookie2", 48 | "cross-origin-embedder-policy", 49 | "cross-origin-opener-policy", 50 | "cross-origin-resource-policy", 51 | "date", 52 | "device-memory", 53 | "downlink", 54 | "dpr", 55 | "early-data", 56 | "ect", 57 | "etag", 58 | "expect", 59 | "expect-ct", 60 | "expires", 61 | "feature-policy", 62 | "forwarded", 63 | "from", 64 | "host", 65 | "if-match", 66 | "if-modified-since", 67 | "if-none-match", 68 | "if-range", 69 | "if-unmodified-since", 70 | "keep-alive", 71 | "large-allocation", 72 | "last-event-id", 73 | "last-modified", 74 | "link", 75 | "location", 76 | "max-forwards", 77 | "nel", 78 | "origin", 79 | "origin-isolation", 80 | "ping-from", 81 | "ping-to", 82 | "pragma", 83 | "proxy-authenticate", 84 | "proxy-authorization", 85 | "public-key-pins", 86 | "public-key-pins-report-only", 87 | "push-policy", 88 | "range", 89 | "referer", 90 | "referrer-policy", 91 | "report-to", 92 | "retry-after", 93 | "rtt", 94 | "save-data", 95 | "sec-ch-ua", 96 | "sec-ch-ua-arch", 97 | "sec-ch-ua-bitness", 98 | "sec-ch-ua-full-version", 99 | "sec-ch-ua-full-version-list", 100 | "sec-ch-ua-mobile", 101 | "sec-ch-ua-model", 102 | "sec-ch-ua-platform", 103 | "sec-ch-ua-platform-version", 104 | "sec-fetch-dest", 105 | "sec-fetch-mode", 106 | "sec-fetch-site", 107 | "sec-fetch-user", 108 | "sec-websocket-accept", 109 | "sec-websocket-extensions", 110 | "sec-websocket-key", 111 | "sec-websocket-protocol", 112 | "sec-websocket-version", 113 | "server", 114 | "server-timing", 115 | "service-worker-allowed", 116 | "set-cookie", 117 | "set-cookie2", 118 | "signature", 119 | "signed-headers", 120 | "sourcemap", 121 | "strict-transport-security", 122 | "te", 123 | "timing-allow-origin", 124 | "trailer", 125 | "transfer-encoding", 126 | "upgrade", 127 | "upgrade-insecure-requests", 128 | USER_AGENT, 129 | "vary", 130 | "via", 131 | "viewport-width", 132 | "warning", 133 | "width", 134 | "www-authenticate", 135 | "x-content-type-options", 136 | "x-dns-prefetch-control", 137 | "x-download-options", 138 | "x-firefox-spdy", 139 | "x-forwarded-for", 140 | "x-forwarded-host", 141 | "x-forwarded-proto", 142 | "x-frame-options", 143 | "x-permitted-cross-domain-policies", 144 | "x-pingback", 145 | "x-powered-by", 146 | "x-requested-with", 147 | "x-robots-tag", 148 | "x-ua-compatible", 149 | "x-xss-protection", 150 | ] 151 | .contains(&header_name) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /crates/cli/src/request/headers.rs: -------------------------------------------------------------------------------- 1 | use crate::core::{Workspace, WorkspaceData}; 2 | use crate::{rh_homepage, rh_name, rh_version}; 3 | use reqwest::header::{HeaderMap, HeaderValue}; 4 | 5 | use super::header; 6 | 7 | pub fn upgrade(args: &Workspace, headers: &mut HeaderMap) { 8 | if args.is_json() { 9 | if !headers.contains_key(header::CONTENT_TYPE) { 10 | headers.append(header::CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap()); 11 | } 12 | if !headers.contains_key(header::ACCEPT) { 13 | headers.append(header::ACCEPT, HeaderValue::from_str("application/json").unwrap()); 14 | } 15 | } 16 | if args.is_form() && !headers.contains_key(header::CONTENT_TYPE) { 17 | headers.append(header::CONTENT_TYPE, HeaderValue::from_str("application/x-www-form-urlencoded").unwrap()); 18 | } 19 | 20 | if !headers.contains_key(header::USER_AGENT) { 21 | headers.append( 22 | header::USER_AGENT, 23 | HeaderValue::from_str(&format!("{}/{} {}", rh_name!(), rh_version!(), rh_homepage!(),)).unwrap(), 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /crates/cli/src/request/mod.rs: -------------------------------------------------------------------------------- 1 | mod body; 2 | mod certificate; 3 | 4 | pub(crate) mod header; 5 | pub(crate) mod headers; 6 | use crate::core::{Error, Result, Workspace}; 7 | use body::Body; 8 | use std::time::Duration; 9 | 10 | pub type Response = reqwest::blocking::Response; 11 | pub type Method = reqwest::Method; 12 | pub type HeaderMap = reqwest::header::HeaderMap; 13 | 14 | pub fn execute(args: &Workspace, req_number: u8, headers: &HeaderMap) -> Result { 15 | let mut client_builder = reqwest::blocking::Client::builder() 16 | .default_headers(headers.clone()) 17 | .gzip(false) 18 | .timeout(Duration::from_secs(10)); 19 | 20 | if let Some(cafile) = args.certificate_authority_file.as_ref() { 21 | let cert = certificate::load(cafile)?; 22 | client_builder = client_builder.add_root_certificate(cert); 23 | } 24 | 25 | let client = client_builder.build()?; 26 | let method = args.method.clone(); 27 | let url = &args.urls[req_number as usize]; 28 | let response = client.request(method, url).body_if_items(args).send()?; 29 | Ok(response) 30 | } 31 | 32 | impl From for Error { 33 | fn from(err: reqwest::Error) -> Error { 34 | Error::Request(err.to_string()) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /crates/cli/src/shell/error.rs: -------------------------------------------------------------------------------- 1 | use super::{Render, enable_colors}; 2 | use crate::theme::style::Color; 3 | use std::{ 4 | fmt::Display, 5 | io::{Result, Write}, 6 | }; 7 | 8 | pub struct ErrorRender { 9 | message: T, 10 | } 11 | 12 | impl ErrorRender { 13 | pub fn new(message: T) -> Self { 14 | Self { message } 15 | } 16 | } 17 | 18 | impl Render for ErrorRender { 19 | #[inline] 20 | fn is_style_active(&self) -> bool { 21 | enable_colors() 22 | } 23 | 24 | #[inline] 25 | fn write(&self, writer: &mut W) -> Result<()> { 26 | self.write_with_style(writer, "Error: ".as_bytes(), &Color::Red.bold())?; 27 | writeln!(writer, "{}", self.message)?; 28 | Ok(()) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /crates/cli/src/shell/form.rs: -------------------------------------------------------------------------------- 1 | use super::Render; 2 | use serde::Serialize; 3 | use std::io::{Result, Write}; 4 | 5 | pub struct FormRender<'a, T> { 6 | value: &'a T, 7 | // compact: bool, 8 | style_enabled: bool, 9 | } 10 | 11 | impl<'a, T: Serialize> FormRender<'a, T> { 12 | pub fn new(value: &'a T, _compact: bool, style_enabled: bool) -> Self { 13 | Self { 14 | value, 15 | // compact, 16 | style_enabled, 17 | } 18 | } 19 | } 20 | 21 | impl<'a, T: Serialize> Render for FormRender<'a, T> { 22 | #[inline] 23 | fn is_style_active(&self) -> bool { 24 | self.style_enabled 25 | } 26 | 27 | #[inline] 28 | fn write(&self, writer: &mut W) -> Result<()> { 29 | match serde_urlencoded::to_string(self.value) { 30 | Ok(buffer) => { 31 | writer.write_all(buffer.as_bytes())?; 32 | } 33 | Err(_) => { 34 | writer.write_all(b"Can't render the items as URL encoded")?; 35 | } 36 | }; 37 | Ok(()) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /crates/cli/src/shell/json.rs: -------------------------------------------------------------------------------- 1 | use super::Render; 2 | use ansi_term::{Color as AnsiTermColor, Style}; 3 | use colored_json::{ColoredFormatter, CompactFormatter, PrettyFormatter, Styler}; 4 | use serde::Serialize; 5 | use serde_json::ser::Formatter; 6 | use std::io::{Result, Write}; 7 | 8 | pub struct JsonRender<'a, T> { 9 | value: &'a T, 10 | compact: bool, 11 | style_enabled: bool, 12 | } 13 | 14 | impl<'a, T: Serialize> JsonRender<'a, T> { 15 | pub fn new(value: &'a T, compact: bool, style_enabled: bool) -> Self { 16 | Self { value, compact, style_enabled } 17 | } 18 | } 19 | 20 | impl<'a, T: Serialize> Render for JsonRender<'a, T> { 21 | #[inline] 22 | fn is_style_active(&self) -> bool { 23 | self.style_enabled 24 | } 25 | 26 | #[inline] 27 | fn write(&self, writer: &mut W) -> Result<()> { 28 | if self.compact { 29 | self.write_with_formatter(writer, CompactFormatter) 30 | } else { 31 | self.write_with_formatter(writer, PrettyFormatter::new()) 32 | } 33 | } 34 | } 35 | 36 | impl<'a, T: Serialize> JsonRender<'a, T> { 37 | #[inline] 38 | fn write_with_formatter(&self, writer: W, formatter: F) -> Result<()> { 39 | let formatter = ColoredFormatter::with_styler(formatter, self.style()); 40 | let mut serializer = serde_json::Serializer::with_formatter(writer, formatter); 41 | self.value.serialize(&mut serializer)?; 42 | Ok(()) 43 | } 44 | 45 | #[inline] 46 | fn style(&self) -> Styler { 47 | Styler { 48 | object_brackets: Style::new(), 49 | array_brackets: Style::new().fg(AnsiTermColor::Red), 50 | key: Style::new().fg(AnsiTermColor::Blue), 51 | string_value: Style::new().fg(AnsiTermColor::Green), 52 | integer_value: Style::new().fg(AnsiTermColor::Purple), 53 | float_value: Style::new().fg(AnsiTermColor::Purple), 54 | bool_value: Style::new().fg(AnsiTermColor::Yellow), 55 | nil_value: Style::new().fg(AnsiTermColor::Cyan), 56 | string_include_quotation: true, 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /crates/cli/src/shell/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod error; 2 | pub(crate) mod form; 3 | pub(crate) mod json; 4 | pub mod os; 5 | pub(crate) mod stream; 6 | 7 | use self::os::OsDirs; 8 | use crate::theme::style::{Color, Style}; 9 | use ansi_term::Color as AnsiTermColor; 10 | use ansi_term::Style as AnsiTermStyle; 11 | use std::io::{Result, Write}; 12 | 13 | #[inline] 14 | pub fn enable_colors() -> bool { 15 | #[cfg(windows)] 16 | return ansi_term::enable_ansi_support().is_ok(); 17 | #[cfg(not(windows))] 18 | true 19 | } 20 | 21 | pub struct Shell<'a, OD, O, E> { 22 | os_dirs: &'a OD, 23 | out: O, 24 | err: E, 25 | } 26 | 27 | impl<'a, OD: OsDirs, O: Write, E: Write> Shell<'a, OD, O, E> { 28 | pub fn new(os_dirs: &'a OD, out: O, err: E) -> Self { 29 | Self { os_dirs, out, err } 30 | } 31 | 32 | pub fn out(&mut self, render: R) -> Result<()> { 33 | render.write(&mut self.out)?; 34 | Ok(()) 35 | } 36 | pub fn err(&mut self, render: R) -> Result<()> { 37 | render.write(&mut self.err)?; 38 | Ok(()) 39 | } 40 | 41 | pub fn os_dirs(&self) -> &OD { 42 | self.os_dirs 43 | } 44 | 45 | #[inline] 46 | pub fn enable_colors(&self) -> bool { 47 | enable_colors() 48 | } 49 | 50 | // pub fn flush(&mut self) -> Result<()> { 51 | // self.out.flush()?; 52 | // self.err.flush() 53 | // } 54 | } 55 | 56 | pub trait Render { 57 | fn write(&self, writer: &mut W) -> Result<()>; 58 | 59 | fn is_style_active(&self) -> bool; 60 | 61 | fn write_newline(&self, writer: &mut W) -> Result<()> { 62 | writer.write_all(b"\n")?; 63 | Ok(()) 64 | } 65 | 66 | fn write_with_style(&self, writer: &mut W, buf: &[u8], style: &Style) -> Result<()> { 67 | if self.is_style_active() { 68 | AnsiTermStyle { 69 | is_bold: style.is_bold, 70 | is_dimmed: style.is_dimmed, 71 | foreground: to_ansi_term_color(style.forecolor), 72 | ..AnsiTermStyle::default() 73 | } 74 | .paint(buf) 75 | .write_to(writer)?; 76 | } else { 77 | writer.write_all(buf)?; 78 | } 79 | 80 | Ok(()) 81 | } 82 | } 83 | 84 | #[inline] 85 | fn to_ansi_term_color(color: Option) -> Option { 86 | color.map(|color| color.into()) 87 | } 88 | 89 | impl From for AnsiTermColor { 90 | fn from(color: Color) -> AnsiTermColor { 91 | match color { 92 | Color::Black => AnsiTermColor::Black, 93 | Color::Red => AnsiTermColor::Red, 94 | Color::Green => AnsiTermColor::Green, 95 | Color::Yellow => AnsiTermColor::Yellow, 96 | Color::Blue => AnsiTermColor::Blue, 97 | Color::Purple => AnsiTermColor::Purple, 98 | Color::Cyan => AnsiTermColor::Cyan, 99 | Color::White => AnsiTermColor::White, 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /crates/cli/src/shell/os.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use crate::rh_name; 4 | 5 | pub trait OsDirs { 6 | fn app_path(&self, filename: &str) -> Option; 7 | fn app_config_directory(&self) -> Option; 8 | fn config_directory(&self) -> Option; 9 | } 10 | 11 | #[derive(Default)] 12 | pub struct DefaultOsDirs; 13 | 14 | impl OsDirs for DefaultOsDirs { 15 | fn app_path(&self, filename: &str) -> Option { 16 | self.app_config_directory().map(|path| path.join(filename)) 17 | } 18 | 19 | fn app_config_directory(&self) -> Option { 20 | self.config_directory().map(|path| path.join(rh_name!())) 21 | } 22 | 23 | fn config_directory(&self) -> Option { 24 | dirs::config_dir() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /crates/cli/src/shell/stream.rs: -------------------------------------------------------------------------------- 1 | use atty::{is, Stream}; 2 | 3 | pub fn is_stdout() -> bool { 4 | is(Stream::Stdout) 5 | } 6 | 7 | pub fn is_stdin() -> bool { 8 | is(Stream::Stdin) 9 | } 10 | -------------------------------------------------------------------------------- /crates/cli/src/test/alias.rs: -------------------------------------------------------------------------------- 1 | use super::os::app_config_directory_for_tests_only; 2 | use crate::commands::alias::storage::{ALIAS_FILENAME_PREFIX, ALIAS_FILENAME_SUFFIX, DEFAULT_ALIAS_NAME}; 3 | use std::io::Write; 4 | use std::{ 5 | fs::{self, File}, 6 | path::Path, 7 | }; 8 | 9 | pub const CUSTOM_ALIAS_NAME_1: &str = "alias-one"; 10 | pub const CUSTOM_ALIAS_NAME_2: &str = "alias2"; 11 | pub const EMPTY_ALIAS_NAME: &str = "empty"; 12 | 13 | pub fn alias_filename(name: &str) -> String { 14 | format!( 15 | "{}/{}{}{}", 16 | app_config_directory_for_tests_only().display(), 17 | ALIAS_FILENAME_PREFIX, 18 | name, 19 | ALIAS_FILENAME_SUFFIX 20 | ) 21 | } 22 | 23 | pub fn create_alias_file(name: &str) { 24 | create_alias_file_with_args(name, "-v\n-c"); 25 | } 26 | 27 | pub fn create_alias_file_with_args(name: &str, args: &str) { 28 | let app_config_path = app_config_directory_for_tests_only(); 29 | if !Path::exists(&app_config_path) { 30 | fs::create_dir(app_config_path).expect("Cannot create the app config directory"); 31 | } 32 | let mut file = File::create(alias_filename(name)).expect("Cannot create the alias file"); 33 | file.write_all(args.as_bytes()).expect("Cannot write content in the alias file"); 34 | } 35 | 36 | pub fn create_empty_alias_file(name: &str) { 37 | let mut file = File::create(alias_filename(name)).expect("Cannot create the alias file"); 38 | file.write_all("".as_bytes()).expect("Cannot write content in the alias file"); 39 | } 40 | 41 | pub fn alias_exists(name: &str) -> bool { 42 | Path::new(&alias_filename(name)).exists() 43 | } 44 | 45 | pub fn delete_alias_file(name: &str) { 46 | let _ = fs::remove_file(alias_filename(name)); 47 | } 48 | 49 | pub fn setup() { 50 | std::env::set_var("HOME", app_config_directory_for_tests_only()); 51 | delete_alias_file(DEFAULT_ALIAS_NAME); 52 | delete_alias_file(CUSTOM_ALIAS_NAME_1); 53 | delete_alias_file(CUSTOM_ALIAS_NAME_2); 54 | delete_alias_file(EMPTY_ALIAS_NAME); 55 | } 56 | -------------------------------------------------------------------------------- /crates/cli/src/test/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod alias; 2 | pub(crate) mod os; 3 | 4 | // #[macro_export] 5 | // macro_rules! args { 6 | // () => {{ 7 | // let v = Vec::::new(); 8 | // v 9 | // }}; 10 | // ($($elem:expr),+ $(,)?) => {{ 11 | // let v = vec![ 12 | // $( String::from($elem), )* 13 | // ]; 14 | // v 15 | // }}; 16 | // } 17 | 18 | // #[macro_export] 19 | // macro_rules! arg_alias { 20 | // ($alias:expr) => { 21 | // format!("{}{}", ALIAS_NAME_PREFIX, $alias) 22 | // }; 23 | // } 24 | 25 | // #[macro_export] 26 | // macro_rules! assert_str_eq { 27 | // ($url:expr, $expected:expr) => { 28 | // assert_eq!($url, $expected.to_string()) 29 | // }; 30 | // } 31 | 32 | // mod basics { 33 | // #[test] 34 | // fn macro_args() { 35 | // let args = args![]; 36 | // let expected: Vec = vec![]; 37 | // assert_eq!(args, expected); 38 | 39 | // let args = args!["one", "two", "three"]; 40 | // let expected: Vec = vec!["one".into(), "two".into(), "three".into()]; 41 | // assert_eq!(args, expected); 42 | // } 43 | 44 | // #[test] 45 | // fn macro_assert_url_eq() { 46 | // let url = "http://test.com"; 47 | // assert_str_eq!(url.to_string(), url); 48 | // } 49 | // } 50 | -------------------------------------------------------------------------------- /crates/cli/src/test/os.rs: -------------------------------------------------------------------------------- 1 | use std::{env, path::PathBuf}; 2 | 3 | use crate::shell::os::OsDirs; 4 | 5 | const APP_NAME_FOR_TESTS_ONLY: &str = "rh-test"; 6 | 7 | pub fn app_config_directory_for_tests_only() -> PathBuf { 8 | config_directory_for_tests_only().join(APP_NAME_FOR_TESTS_ONLY) 9 | } 10 | pub fn config_directory_for_tests_only() -> PathBuf { 11 | env::temp_dir() 12 | } 13 | 14 | pub struct TestValidOsDirs; 15 | 16 | impl TestValidOsDirs { 17 | pub fn new() -> Self { 18 | Self {} 19 | } 20 | } 21 | 22 | impl OsDirs for TestValidOsDirs { 23 | fn app_path(&self, filename: &str) -> Option { 24 | self.app_config_directory().map(|path| path.join(filename)) 25 | } 26 | 27 | fn app_config_directory(&self) -> Option { 28 | self.config_directory().map(|path| path.join(APP_NAME_FOR_TESTS_ONLY)) 29 | } 30 | 31 | fn config_directory(&self) -> Option { 32 | Some(config_directory_for_tests_only()) 33 | } 34 | } 35 | 36 | pub struct TestNoOsDirs; 37 | 38 | impl TestNoOsDirs { 39 | pub fn new() -> Self { 40 | Self {} 41 | } 42 | } 43 | 44 | impl OsDirs for TestNoOsDirs { 45 | fn app_path(&self, filename: &str) -> Option { 46 | self.app_config_directory().map(|path| path.join(filename)) 47 | } 48 | 49 | fn app_config_directory(&self) -> Option { 50 | self.config_directory() 51 | } 52 | 53 | fn config_directory(&self) -> Option { 54 | None 55 | } 56 | } 57 | 58 | pub struct TestInvalidOsDirs; 59 | 60 | impl TestInvalidOsDirs { 61 | pub fn new() -> Self { 62 | Self {} 63 | } 64 | } 65 | 66 | impl OsDirs for TestInvalidOsDirs { 67 | fn app_path(&self, filename: &str) -> Option { 68 | self.app_config_directory().map(|path| path.join(filename)) 69 | } 70 | 71 | fn app_config_directory(&self) -> Option { 72 | self.config_directory() 73 | } 74 | 75 | fn config_directory(&self) -> Option { 76 | Some(PathBuf::from("a-dir-that_does-not-exist")) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /crates/cli/src/theme/default.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | style::{Color, Style}, 3 | DirectionTheme, HeaderTheme, RequestTheme, ResponseTheme, Theme, 4 | }; 5 | 6 | #[cfg_attr(test, derive(Debug))] 7 | #[derive(Clone, Copy)] 8 | pub struct DefaultTheme {} 9 | #[derive(Clone, Copy)] 10 | pub struct DefaultReponseTheme {} 11 | #[derive(Clone, Copy)] 12 | pub struct DefaultRequestTheme {} 13 | 14 | impl RequestTheme for DefaultRequestTheme { 15 | fn as_header(&self) -> &dyn HeaderTheme { 16 | self 17 | } 18 | fn as_direction(&self) -> &dyn DirectionTheme { 19 | self 20 | } 21 | fn primary(&self) -> Style { 22 | Color::Purple.normal() 23 | } 24 | fn secondary(&self) -> Style { 25 | Color::Purple.normal() 26 | } 27 | fn method(&self) -> Style { 28 | Color::Purple.bold() 29 | } 30 | fn url(&self) -> Style { 31 | Color::Purple.normal_newline() 32 | } 33 | } 34 | impl HeaderTheme for DefaultRequestTheme { 35 | fn header_name(&self, standard: bool) -> Style { 36 | crate::ifelse!(standard, self.primary(), self.secondary()) 37 | } 38 | fn header_value(&self, _: bool) -> Style { 39 | Style::newline() 40 | } 41 | } 42 | impl DirectionTheme for DefaultRequestTheme { 43 | fn direction(&self, standard: bool) -> Style { 44 | crate::ifelse!(standard, self.primary(), self.secondary()) 45 | } 46 | } 47 | 48 | impl ResponseTheme for DefaultReponseTheme { 49 | fn as_header(&self) -> &dyn HeaderTheme { 50 | self 51 | } 52 | fn as_direction(&self) -> &dyn DirectionTheme { 53 | self 54 | } 55 | fn primary(&self) -> Style { 56 | Color::Green.normal() 57 | } 58 | fn secondary(&self) -> Style { 59 | Color::Cyan.normal() 60 | } 61 | fn version(&self) -> Style { 62 | Color::Green.normal() 63 | } 64 | fn status(&self) -> Style { 65 | Color::Green.bold_newline() 66 | } 67 | } 68 | impl HeaderTheme for DefaultReponseTheme { 69 | fn header_name(&self, standard: bool) -> Style { 70 | crate::ifelse!(standard, self.primary(), self.secondary()) 71 | } 72 | fn header_value(&self, _: bool) -> Style { 73 | Style::newline() 74 | } 75 | } 76 | impl DirectionTheme for DefaultReponseTheme { 77 | fn direction(&self, standard: bool) -> Style { 78 | crate::ifelse!(standard, self.primary(), self.secondary()) 79 | } 80 | } 81 | 82 | impl Theme for DefaultTheme { 83 | fn request(&self) -> Box { 84 | Box::new(DefaultRequestTheme {}) 85 | } 86 | fn response(&self) -> Box { 87 | Box::new(DefaultReponseTheme {}) 88 | } 89 | } 90 | 91 | impl DefaultTheme { 92 | pub fn new() -> DefaultTheme { 93 | DefaultTheme {} 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /crates/cli/src/theme/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod default; 2 | pub(crate) mod style; 3 | 4 | use style::Style; 5 | 6 | pub trait Theme { 7 | fn request(&self) -> Box; 8 | fn response(&self) -> Box; 9 | } 10 | 11 | pub trait DirectionTheme { 12 | fn direction(&self, standard: bool) -> Style; 13 | } 14 | 15 | pub trait HeaderTheme { 16 | fn header_name(&self, standard: bool) -> Style; 17 | fn header_value(&self, standard: bool) -> Style; 18 | } 19 | 20 | pub trait RequestTheme: HeaderTheme + DirectionTheme { 21 | fn as_header(&self) -> &dyn HeaderTheme; 22 | fn as_direction(&self) -> &dyn DirectionTheme; 23 | fn primary(&self) -> Style; 24 | fn secondary(&self) -> Style; 25 | fn method(&self) -> Style; 26 | fn url(&self) -> Style; 27 | } 28 | 29 | pub trait ResponseTheme: HeaderTheme + DirectionTheme { 30 | fn as_header(&self) -> &dyn HeaderTheme; 31 | fn as_direction(&self) -> &dyn DirectionTheme; 32 | fn primary(&self) -> Style; 33 | fn secondary(&self) -> Style; 34 | fn version(&self) -> Style; 35 | fn status(&self) -> Style; 36 | } 37 | 38 | #[cfg(test)] 39 | impl core::fmt::Debug for dyn Theme { 40 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 41 | write!(f, "Theme") 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /crates/cli/src/theme/style.rs: -------------------------------------------------------------------------------- 1 | #[derive(Default)] 2 | pub struct Style { 3 | pub forecolor: Option, 4 | pub backcolor: Option, 5 | pub is_bold: bool, 6 | pub is_dimmed: bool, 7 | pub newline: bool, 8 | } 9 | 10 | #[allow(dead_code)] 11 | #[derive(Clone, Copy)] 12 | pub enum Color { 13 | Black, 14 | Red, 15 | Green, 16 | Yellow, 17 | Blue, 18 | Purple, 19 | Cyan, 20 | White, 21 | } 22 | 23 | impl Style { 24 | pub fn newline() -> Style { 25 | Style { 26 | newline: true, 27 | ..Default::default() 28 | } 29 | } 30 | } 31 | 32 | impl Color { 33 | pub fn normal(self) -> Style { 34 | Style { 35 | forecolor: Some(self), 36 | ..Default::default() 37 | } 38 | } 39 | pub fn normal_newline(self) -> Style { 40 | Style { 41 | forecolor: Some(self), 42 | newline: true, 43 | ..Default::default() 44 | } 45 | } 46 | pub fn bold(self) -> Style { 47 | Style { 48 | forecolor: Some(self), 49 | is_bold: true, 50 | ..Default::default() 51 | } 52 | } 53 | pub fn bold_newline(self) -> Style { 54 | Style { 55 | forecolor: Some(self), 56 | is_bold: true, 57 | newline: true, 58 | ..Default::default() 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /crates/cli/tests/certificate.rs: -------------------------------------------------------------------------------- 1 | // #[cfg(test)] 2 | // mod tests { 3 | 4 | // // #[test] 5 | // // fn invalid_pem() { 6 | // // let res = certificate("ca.pem", b"invalid pem"); 7 | // // assert!(res.is_err()); 8 | // // } 9 | 10 | // #[test] 11 | // fn invalid_der() { 12 | // let res = certificate("ca.der", b"invalid der").unwrap(); 13 | // // assert!(res); 14 | // } 15 | // } 16 | -------------------------------------------------------------------------------- /crates/cli/tests/macros.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! args { 3 | () => {{ 4 | let v = Vec::::new(); 5 | v 6 | }}; 7 | ($($elem:expr),+ $(,)?) => {{ 8 | let v = vec![ 9 | $( String::from($elem), )* 10 | ]; 11 | v 12 | }}; 13 | } 14 | 15 | #[macro_export] 16 | macro_rules! arg_alias { 17 | ($alias:expr) => { 18 | format!("{}{}", ALIAS_NAME_PREFIX, $alias) 19 | }; 20 | } 21 | 22 | #[macro_export] 23 | macro_rules! assert_str_eq { 24 | ($url:expr, $expected:expr) => { 25 | assert_eq!($url, $expected.to_string()) 26 | }; 27 | } 28 | 29 | mod basics { 30 | #[test] 31 | fn macro_args() { 32 | let args = args![]; 33 | let expected: Vec = vec![]; 34 | assert_eq!(args, expected); 35 | 36 | let args = args!["one", "two", "three"]; 37 | let expected: Vec = vec!["one".into(), "two".into(), "three".into()]; 38 | assert_eq!(args, expected); 39 | } 40 | 41 | #[test] 42 | fn macro_assert_url_eq() { 43 | let url = "http://test.com"; 44 | assert_str_eq!(url.to_string(), url); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /crates/cli/tests/rh.rs: -------------------------------------------------------------------------------- 1 | use httpmock::prelude::*; 2 | use rh::shell::{ 3 | os::{DefaultOsDirs, OsDirs}, 4 | Shell, 5 | }; 6 | 7 | fn shell<'a, OD: OsDirs>(os_dirs: &'a OD) -> Shell<'a, OD, Vec, Vec> { 8 | let out = Vec::new(); 9 | let err = Vec::new(); 10 | Shell::new(os_dirs, out, err) 11 | } 12 | 13 | #[test] 14 | fn no_args() { 15 | let os_dirs = DefaultOsDirs::default(); 16 | let mut shell = shell(&os_dirs); 17 | 18 | let mut args = rh_test::args![]; 19 | let exit_code = rh::run(&mut args, &mut shell); 20 | assert_eq!(exit_code, 100); 21 | } 22 | 23 | #[test] 24 | fn get_localhost() { 25 | let server = MockServer::start(); 26 | let http_mock = server.mock(|when, then| { 27 | when.path("/"); 28 | then.status(200).header("content-type", "text/plain").body("ohi"); 29 | }); 30 | let url = server.url("/"); 31 | 32 | let os_dirs = DefaultOsDirs::default(); 33 | let mut shell = shell(&os_dirs); 34 | 35 | let mut args = rh_test::args![url]; 36 | let exit_code = rh::run(&mut args, &mut shell); 37 | assert_eq!(exit_code, 0); 38 | http_mock.assert(); 39 | } 40 | -------------------------------------------------------------------------------- /crates/test/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rh_test" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["twigly"] 6 | license = "MIT" 7 | description = "A user-friendly command-line tool to request HTTP APis" 8 | readme = "README.md" 9 | homepage = "https://github.com/twigly/rh" 10 | repository = "https://github.com/twigly/rh" 11 | keywords = ["cli", "http", "terminal", "tool", "devops"] 12 | categories = ["command-line-utilities"] 13 | 14 | [dependencies] 15 | -------------------------------------------------------------------------------- /crates/test/README.md: -------------------------------------------------------------------------------- 1 | # Rust HTTP library for tests 2 | 3 | Used by unit and integration tests. 4 | -------------------------------------------------------------------------------- /crates/test/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! args { 3 | () => {{ 4 | let v = Vec::::new(); 5 | v 6 | }}; 7 | ($($elem:expr),+ $(,)?) => {{ 8 | let v = vec![ 9 | $( String::from($elem), )* 10 | ]; 11 | v 12 | }}; 13 | } 14 | 15 | #[macro_export] 16 | macro_rules! arg_alias { 17 | ($alias:expr) => { 18 | format!("{}{}", ALIAS_NAME_PREFIX, $alias) 19 | }; 20 | } 21 | 22 | #[macro_export] 23 | macro_rules! assert_str_eq { 24 | ($url:expr, $expected:expr) => { 25 | assert_eq!($url, $expected.to_string()) 26 | }; 27 | } 28 | 29 | mod basics { 30 | #[test] 31 | fn macro_args() { 32 | let args = args![]; 33 | let expected: Vec = vec![]; 34 | assert_eq!(args, expected); 35 | 36 | let args = args!["one", "two", "three"]; 37 | let expected: Vec = vec!["one".into(), "two".into(), "three".into()]; 38 | assert_eq!(args, expected); 39 | } 40 | 41 | #[test] 42 | fn macro_assert_url_eq() { 43 | let url = "http://test.com"; 44 | assert_str_eq!(url.to_string(), url); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /doc/alias.md: -------------------------------------------------------------------------------- 1 | # Alias 2 | 3 | ## How to configure? 4 | 5 | You can change ```rh``` default behaviour and create aliases you can reuse easily. 6 | 7 | To see all the aliases available, the syntax is: 8 | 9 | ```bash 10 | > rh alias --list 11 | ``` 12 | 13 | To update a configuration, the syntax is: 14 | 15 | ```bash 16 | > rh alias [@alias] 17 | ``` 18 | 19 | Please note that ```@alias``` is optional. If not specified it's the ```default``` alias. ```@alias``` must be lower-case. 20 | 21 | ```options``` can be any ```rh``` options. 22 | 23 | ## Default alias 24 | 25 | The ```default``` alias is used if no alias is specified. For example, if you want to show the response headers (```--header``` or ```-h```): 26 | 27 | ```bash 28 | > rh alias -h 29 | ``` 30 | 31 | You can select multiple options at the same time: 32 | 33 | ```bash 34 | > rh alias --header --compact 35 | ``` 36 | 37 | Or the same but shorter: 38 | 39 | ```bash 40 | > rh alias -hc 41 | ``` 42 | 43 | ## Custom alias 44 | 45 | You can create an alias to show the ```-U```RL and method + to show the response ```-h```eaders + to show a ```-c```ompact response: 46 | 47 | ```bash 48 | > rh alias @my-alias -Uhc 49 | ``` 50 | 51 | ## How to use an alias 52 | 53 | You can use the "my-alias" alias created above to show the URL, method, response headers, and compact the response body: 54 | 55 | ```bash 56 | > rh @my-alias https://httpbin.org/image/jpeg 57 | ``` 58 | 59 | You can use also the previous default alias that was built with the options ```-hc```: 60 | 61 | ```bash 62 | > rh https://httpbin.org/image/jpeg 63 | ``` 64 | 65 | ## Delete an alias 66 | 67 | You can delete any alias you created, including the default alias. To delete the default alias: 68 | 69 | ```bash 70 | > rh alias --delete 71 | ``` 72 | 73 | To delete the alias "my-alias": 74 | 75 | ```bash 76 | > rh alias --delete @my-alias 77 | ``` 78 | 79 | ## List all aliases 80 | 81 | As simple as: 82 | 83 | ```bash 84 | > rh alias --list 85 | ``` 86 | 87 | ## More options in the future 88 | 89 | The following default options are not available yet. Once available, these options will be available in ```rh``` in order for the aliases to be more flexible: 90 | 91 | ``` 92 | --hostname=localhost 93 | --port=80 94 | --secure-port=443 95 | --method=GET 96 | --method-if-body=POST 97 | --method-if-pipe=POST 98 | ``` 99 | -------------------------------------------------------------------------------- /doc/authentication.md: -------------------------------------------------------------------------------- 1 | # Authentication 2 | 3 | Feature not available yet. 4 | 5 | ```bash 6 | > rh --basic=my-user:my-pass https://httpbin.org/basic-auth/my-user/my-pass 7 | ``` 8 | 9 | ```bash 10 | > rh --digest=my-user:my-pass httpbin.org/digest-auth/rh/my-user/my-pass 11 | ``` 12 | 13 | ```bash 14 | > rh --bearer=token https://httpbin.org/bearer 15 | ``` 16 | -------------------------------------------------------------------------------- /doc/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for considering helping the ```rh``` project. There are many ways you can help: using ```rh``` and [reporting bugs](https://github.com/twigly/rh/issues), making additions and improvements to ```rh```, documentation, logo, package manager... 4 | 5 | ## Reporting bugs 6 | 7 | Please file a [GitHub issue](https://github.com/twigly/rh/issues). Include as much information as possible. 8 | 9 | Feel free to file [GitHub issues](https://github.com/twigly/rh/issues) to get help, or ask a question. 10 | 11 | ## Code changes 12 | 13 | Some ideas and guidelines for contributions: 14 | 15 | - For large features, you can file an issue prior to starting work. 16 | - Feel free to submit a PR even if the work is not totally finished, for feedback or to hand-over. 17 | - Some [ideas here](todo.md) 18 | 19 | ## Package managers 20 | 21 | - Having ```rh``` available on various platform would be great 22 | 23 | ## Any help is welcome 24 | 25 | He's not developing anything because he's busy sleeping 20 hours a day, but he helps his way... 26 | 27 | ![Ginger](ginger-128.jpeg) 28 | -------------------------------------------------------------------------------- /doc/examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | In the following examples, you can add the flags ```-uhH``` to show more details. 4 | 5 | - ```-u``` to show the URL and method 6 | - ```-h``` to show the response headers 7 | - ```-H``` to show the request headers 8 | 9 | More information with: 10 | 11 | ```bash 12 | > rh --help 13 | ``` 14 | 15 | ## Basics 16 | 17 | Let's start with "Hello, World!": 18 | 19 | ```bash 20 | > rh httpbin.org/get 21 | ``` 22 | 23 | You can POST a request (```rh``` will default to POST because there is a body): 24 | 25 | ```bash 26 | > rh httpbin.org/post id=rh 27 | ``` 28 | 29 | A POST request with headers but no body: 30 | 31 | ```bash 32 | > rh POST httpbin.org/post X-key1:true X-key2:true 33 | ``` 34 | 35 | ## Headers and items 36 | 37 | The separator ```:``` is used to create headers: 38 | 39 | ```bash 40 | > rh httpbin.org/get key:Value 41 | ``` 42 | 43 | The separator ```=``` is used to create items to POST (if there are items then the method is POST): 44 | 45 | ```bash 46 | > rh httpbin.org/post key=Value 47 | ``` 48 | 49 | ## Localhost 50 | 51 | To run the examples of this "localhost" section you need a local server. In the following examples, if you don't specify a host, ```localhost``` will be the default host. Once the config feature is available, you'll be able to change the default host. 52 | 53 | Basic: 54 | 55 | ```bash 56 | > rh http://localhost/test 57 | ``` 58 | 59 | Don't be bothered with the localhost domain: 60 | 61 | ```bash 62 | > rh /test 63 | ``` 64 | 65 | Or : 66 | 67 | ```bash 68 | > rh : 69 | ``` 70 | 71 | Localhost with a particular port: 72 | 73 | ```bash 74 | > rh :9200 75 | ``` 76 | 77 | ```bash 78 | > rh :9200/_cluster/health 79 | ``` 80 | 81 | ## Config (not available yet) 82 | 83 | You can create a config named ```dev``` (this config says to POST the body ```id=rh``` to ```httpbin.org/post```): 84 | 85 | ```bash 86 | > rh dev httpbin.org/post id=rh 87 | ``` 88 | 89 | Let's say you have Elasticsearch running on the ```elasticsearch``` domain, you can define the following config ```ei``` (that would stand for Elasticsearch Indices): 90 | 91 | ```bash 92 | > rh config ei elasticsearch:9200/_cat/indices/*,-.*?v&s=index 93 | ``` 94 | 95 | Then you can just run the following command to show the Elasticsearch indices: 96 | 97 | ```bash 98 | > rh ei 99 | ``` 100 | 101 | ## Data 102 | 103 | You can POST data using pipes: 104 | 105 | ```bash 106 | > echo "Hello, World!" | rh httpbin.org/post 107 | ``` 108 | 109 | You can POST JSON (JSON is the default format): 110 | 111 | ```bash 112 | > rh https://httpbin.org/anything key1=1 113 | ``` 114 | 115 | You can POST data using the URL encoded format: 116 | 117 | ```bash 118 | > rh https://httpbin.org/anything key1=1 --form 119 | ``` 120 | 121 | Or using the raw flag: 122 | 123 | ```bash 124 | > rh https://httpbin.org/anything --raw='{"key1":1}' Content-Type:application/json 125 | ``` 126 | 127 | Or just plain text: 128 | 129 | ```bash 130 | > rh https://httpbin.org/anything --raw=hello 131 | ``` 132 | 133 | Or multi-lines: 134 | 135 | ```bash 136 | > rh https://httpbin.org/anything --raw=' 137 | { 138 | "inner-planets": ["Mercury", "Venus", "Earth", "Mars"], 139 | "sun": { 140 | "temp": 5778, 141 | "bigger-than-earth": true 142 | } 143 | } 144 | ' 145 | ``` 146 | 147 | ## Files 148 | 149 | You can download a file and save it: 150 | 151 | ```bash 152 | > rh https://httpbin.org/image/jpeg > image.jpeg 153 | ``` 154 | 155 | If you love ```cat``` 🐱, you can upload a file: 156 | 157 | ```bash 158 | > cat info.txt | rh httpbin.org/post 159 | ``` 160 | 161 | The following commmand is not available yet, you can upload a file using the symbol ```@``` and the path: 162 | 163 | ```bash 164 | > rh httpbin.org/post @info.txt 165 | ``` 166 | 167 | ## More or Less 168 | 169 | If the response is output to another program there is no colours: 170 | 171 | ```bash 172 | > rh :9200/_nodes | more 173 | ``` 174 | 175 | But you can preserve the colors with the ```--pretty=color``` option and ```less -R```: 176 | 177 | ```bash 178 | > rh :9200/_nodes --pretty=color | less -R 179 | ``` 180 | 181 | ## SSL Certificates 182 | 183 | You can use self-signed certificates (you can use PEM or DER format): 184 | 185 | ```bash 186 | > rh https://localhost:8080 -v --cafile=rsa/ca.cert 187 | ``` 188 | 189 | The .der extension is required for using the DER format: 190 | 191 | ```bash 192 | > rh https://localhost:8080 -v --cafile=rsa/ca.der 193 | ``` 194 | 195 | ## Some options 196 | 197 | Show the URL and method: 198 | 199 | ```bash 200 | > rh httpbin.org/get -U 201 | ``` 202 | 203 | Show the headers (request and response): 204 | 205 | ```bash 206 | > rh httpbin.org/get -hH 207 | ``` 208 | 209 | Show the URL, method, headers and the response body as a compact form: 210 | 211 | ```bash 212 | > rh httpbin.org/get -UhHc 213 | ``` 214 | 215 | More options: 216 | 217 | ```bash 218 | > rh --help 219 | ``` 220 | -------------------------------------------------------------------------------- /doc/ginger-128.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twigly/rust-http-cli/40555709978446431bd736019a52eb616854fcec/doc/ginger-128.jpeg -------------------------------------------------------------------------------- /doc/help-and-version.md: -------------------------------------------------------------------------------- 1 | # Note 2 | 3 | Not available yet, work in progress. 4 | 5 | # Help 6 | 7 | - ```rh --help``` to show detailed help 8 | - ```rh -h``` to show a short help 9 | 10 | # Help 11 | 12 | - ```rh --version``` to show detailed version (user agent and version) 13 | - ```rh -v``` to show only the version 14 | -------------------------------------------------------------------------------- /doc/install.md: -------------------------------------------------------------------------------- 1 | # Install 2 | 3 | ## Cargo 4 | 5 | ```bash 6 | > cargo install rh 7 | ``` 8 | 9 | ## MacOS 10 | 11 | Not available yet. 12 | 13 | ```bash 14 | > brew install rh 15 | ``` 16 | 17 | ## Ubuntu 18 | 19 | Not available yet. 20 | 21 | ```bash 22 | > sudo apt install rh 23 | ``` 24 | 25 | ## More platforms 26 | 27 | Help welcome ;-) 28 | -------------------------------------------------------------------------------- /doc/json.md: -------------------------------------------------------------------------------- 1 | # JSON 2 | 3 | Items are converted to JSON by default. 4 | 5 | ## Key/value 6 | 7 | Items are a list of key/value. Each key/value is specified as ```key=value```. 8 | 9 | ## Number and string 10 | 11 | If you want to force a number to be as a string, you can use ```/=``` instead of ```=``` 12 | 13 | ```bash 14 | > rh httpbin.org/post number1=123 number2/=456 text=hello 15 | ``` 16 | 17 | The JSON object will be: 18 | 19 | ```json 20 | { 21 | "number1": 123, 22 | "number2": "456", 23 | "text": "hello", 24 | } 25 | ``` 26 | 27 | ## Boolean and string 28 | 29 | If you want to force a boolean to be as a string, you can use ```/=``` instead of ```=``` 30 | 31 | ```bash 32 | > rh httpbin.org/post b1=true b2=false b3=y b4=n b5/=true b6/=false b7/=y b8/=n 33 | ``` 34 | 35 | The JSON object will be: 36 | 37 | ```json 38 | { 39 | "b1": true, 40 | "b2": false, 41 | "b3": true, 42 | "b4": false, 43 | "b5": "true", 44 | "b6": "false", 45 | "b7": "y", 46 | "b8": "n", 47 | } 48 | ``` -------------------------------------------------------------------------------- /doc/todo.md: -------------------------------------------------------------------------------- 1 | # Tasks to do 2 | 3 | You're welcome to help on any of the following tasks for example. 4 | 5 | Everything that improves performance is very welcome. 6 | 7 | ## Package manager 8 | 9 | - [ ] Homebrew (Mac) 10 | - [ ] MacPorts (Mac) 11 | - [ ] Debian (Linux) 12 | - [ ] Fedora (Linux) 13 | - [ ] Ubuntu (Linux) 14 | - [ ] Alpine (Linux) 15 | - [ ] Arch (Linux) 16 | - [ ] nixOS (Linux) 17 | - [ ] openSUSE (Linux) 18 | - [ ] Void Linux (Linux) 19 | - [ ] Gentoo (Linux) 20 | - [ ] Android 21 | - [ ] Chocolatey (Windows) 22 | - [ ] Others... 23 | 24 | ## Benchmark 25 | 26 | - [ ] Comparison between ```cURL``` and ```rh``` 27 | 28 | ## Features 29 | 30 | ### Authentication / proxy 31 | 32 | - [ ] Deal with authentications [see authentication](authentication.md) 33 | - [ ] Deal with proxies 34 | 35 | ### Items / headers 36 | 37 | - [ ] Recognise arrays in data items (ex: ```array=item1,item2,item3```) 38 | - [ ] Recognise files in data items (ex: ```file_content=@/path/file```) 39 | - [ ] Remove headers with ```key:``` and set an empty value with ```"key: "``` 40 | - [ ] Read file content using the symbol ```@``` (for example ```--raw=@/path/file``` or ```key=@/path/file```) 41 | - [ ] Append URL parameters via items 42 | - [ ] Option to sort header and JSON keys (for example ```--sort``` to sort both of them, ```--sort=h``` to sort headers, ```--sort=j``` to sort JSON keys) 43 | 44 | ### Content encoding 45 | 46 | - [ ] Read ```content-encoding=gzip``` (https://httpbin.org/gzip) 47 | - [ ] Read ```content-encoding=brotli``` (https://httpbin.org/brotli) 48 | - [ ] Read ```content-encoding=deflate``` (https://httpbin.org/deflate) 49 | 50 | ### Timeout / redirect 51 | 52 | - [ ] Set a max redirects 53 | - [ ] Set a timeout 54 | - [ ] Show redirects if --verbose 55 | 56 | ### Misc 57 | 58 | - [ ] Multi URLs 59 | - [ ] Add an option ```--pretty=format``` to format without colouring 60 | - [ ] Specify cookies without using the ```cookies``` header (and avoid using ```"``` to escape the ```;``` separator) - maybe not worth (low priority) 61 | - [ ] Completion on available platforms 62 | 63 | ## Performance 64 | 65 | - [ ] ```rh``` performance is very good but it would be nice to review the code and try to optimise 66 | - [ ] the current binary size is acceptable but there are certainly ways to decrease it (without sacrificing performance) 67 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 180 -------------------------------------------------------------------------------- /snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: rust-http-cli 2 | version: git 3 | summary: A user-friendly command-line tool to request HTTP APis 4 | description: | 5 | Rust HTTP Cli (`rh`) is a user-friendly, lightweight and performant command-line tool to request HTTP APis. 6 | You can debug, test and verify any HTTP APi with rh in a simple and efficient way. 7 | `rh` is focused on performance and stability. 8 | You don't need OpenSSL because `rh` is based on Rustls, a modern TLS library alternative to OpenSSL. 9 | 10 | `rh` is a standalone application with no runtime or garbage collector, so it doesn't require Python or Java installed on your machine for example. 11 | `rh` is based on Rust that is a blazing fast and memory-efficient language. 12 | license: MIT 13 | 14 | base: core18 15 | confinement: strict 16 | grade: stable 17 | compression: lzo 18 | 19 | parts: 20 | rust-http-cli: 21 | plugin: rust 22 | source: https://github.com/twigly/rust-http-cli.git 23 | 24 | apps: 25 | rust-http-cli: 26 | command: bin/rh 27 | --------------------------------------------------------------------------------