├── .dockerignore ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── book-cd.yml │ ├── book-ci.yml │ ├── build.yml │ ├── publish-to-ghcr.yml │ ├── release-assets.yml │ └── release-plz.yml ├── .gitignore ├── .markdownlint.yaml ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── curlz ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── src │ ├── bin │ │ └── curlz.rs │ └── curlz │ │ ├── cli │ │ ├── execute.rs │ │ ├── header_args.rs │ │ ├── interactive.rs │ │ ├── mod.rs │ │ └── sub_commands │ │ │ ├── bookmark.rs │ │ │ ├── http_file.rs │ │ │ ├── mod.rs │ │ │ └── request.rs │ │ ├── domain │ │ ├── bookmark │ │ │ ├── collection.rs │ │ │ ├── collection_impl │ │ │ │ ├── bookmark_folder_collection.rs │ │ │ │ ├── mod.rs │ │ │ │ └── snapshots │ │ │ │ │ └── curlz__domain__bookmark__collection_impl__bookmark_folder_collection__tests__should_handle_save_bookmark_command.snap │ │ │ ├── load_bookmark.rs │ │ │ ├── mod.rs │ │ │ └── save_bookmark.rs │ │ ├── environment │ │ │ ├── dot_env.rs │ │ │ ├── env.rs │ │ │ ├── mod.rs │ │ │ └── yaml_env.rs │ │ ├── http │ │ │ ├── http_body.rs │ │ │ ├── http_headers.rs │ │ │ ├── http_method.rs │ │ │ ├── http_request.rs │ │ │ ├── http_uri.rs │ │ │ ├── http_version.rs │ │ │ └── mod.rs │ │ ├── http_lang │ │ │ ├── http-lang-grammar.pest │ │ │ ├── mod.rs │ │ │ └── parse_request.rs │ │ ├── mod.rs │ │ └── request │ │ │ ├── backend │ │ │ ├── invoke_curl.rs │ │ │ └── mod.rs │ │ │ ├── context.rs │ │ │ ├── issue_request.rs │ │ │ └── mod.rs │ │ ├── lib.rs │ │ ├── template │ │ ├── functions │ │ │ ├── auth │ │ │ │ ├── basic.rs │ │ │ │ ├── jwt.rs │ │ │ │ └── mod.rs │ │ │ ├── chrono │ │ │ │ ├── mod.rs │ │ │ │ └── timestamp.rs │ │ │ ├── mod.rs │ │ │ ├── process_env.rs │ │ │ └── prompt.rs │ │ ├── mod.rs │ │ └── variables.rs │ │ ├── test_utils.rs │ │ └── utils.rs └── tests │ ├── basics.rs │ ├── fixtures │ └── send-payload-as-post.http │ └── testlib.rs ├── docker ├── Dockerfile ├── Makefile └── entrypoint.sh ├── docs ├── book.toml └── src │ ├── README.md │ ├── SUMMARY.md │ └── template-functions.md ├── examples ├── README.md └── http-file │ ├── README.md │ ├── basic-auth-header.http │ ├── post-to-httpbin.http │ └── with_trailing_whitespaces.http ├── renovate.json └── resources └── demo.gif /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !docker/ 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: sassman 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/book-cd.yml: -------------------------------------------------------------------------------- 1 | name: Book CD 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | workflow_dispatch: # Allow manual triggers 8 | push: 9 | tags: 10 | - 'v[0-9]+.[0-9]+.[0-9]+' 11 | - 'v[0-9]+.[0-9]+.[0-9]-alpha.[0-9]+' 12 | - 'v[0-9]+.[0-9]+.[0-9]-beta.[0-9]+' 13 | - 'v[0-9]+.[0-9]+.[0-9]-rc.[0-9]+' 14 | 15 | jobs: 16 | deploy: 17 | runs-on: ubuntu-22.04 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Setup mdbook 21 | uses: peaceiris/actions-mdbook@v1 22 | - run: mdbook build docs 23 | - name: Deploy 24 | uses: peaceiris/actions-gh-pages@v3 25 | with: 26 | github_token: ${{ secrets.GH_PAT }} 27 | publish_dir: ./docs/book 28 | -------------------------------------------------------------------------------- /.github/workflows/book-ci.yml: -------------------------------------------------------------------------------- 1 | name: Book CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - "docs/**" 9 | pull_request: 10 | paths: 11 | - "docs/**" 12 | 13 | jobs: 14 | 15 | deploy-test: 16 | runs-on: ubuntu-22.04 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Setup mdBook 20 | uses: peaceiris/actions-mdbook@v1 21 | - run: mdbook build docs 22 | 23 | doc-test: 24 | runs-on: ubuntu-22.04 25 | steps: 26 | - uses: actions/checkout@v3 27 | - name: Setup mdBook 28 | uses: peaceiris/actions-mdbook@v1 29 | - run: mdbook test docs 30 | 31 | markdown-lint: 32 | runs-on: ubuntu-22.04 33 | steps: 34 | - uses: actions/checkout@v3 35 | - name: Lint all files recursively 36 | uses: avto-dev/markdown-lint@v1 37 | with: 38 | config: '.markdownlint.yaml' 39 | args: 'docs/**/*.md' 40 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Faster 2 | on: 3 | push: 4 | branches: [ '*' ] 5 | paths-ignore: 6 | - "**/docs/**" 7 | - "**.md" 8 | pull_request: 9 | branches: [ main ] 10 | workflow_call: 11 | 12 | jobs: 13 | check: 14 | name: build crate 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | version: [ 'macos-latest', 'ubuntu-latest', 'windows-latest'] 19 | rust: [ nightly, stable ] 20 | runs-on: ${{ matrix.version }} 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: setup | rust 24 | uses: actions-rs/toolchain@v1 25 | with: 26 | toolchain: stable 27 | default: true 28 | profile: minimal 29 | components: clippy, rustfmt 30 | - uses: Swatinem/rust-cache@v2 31 | - run: cargo install cargo-insta 32 | - run: cargo check 33 | continue-on-error: ${{ matrix.rust == 'nightly' }} 34 | - run: cargo fmt --all -- --check 35 | continue-on-error: ${{ matrix.rust == 'nightly' }} 36 | - run: cargo clippy --all-targets --all-features -- -D warnings 37 | continue-on-error: ${{ matrix.rust == 'nightly' }} 38 | - run: cargo test --all-features --all --locked -- -Z unstable-options 39 | continue-on-error: ${{ matrix.rust == 'nightly' }} 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | - run: cargo insta test 43 | continue-on-error: ${{ matrix.rust == 'nightly' }} 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | - name: smoke tests 47 | run: | 48 | cargo run -- --version 49 | cargo run -- --help 50 | 51 | coverage: 52 | name: coverage report 53 | runs-on: ubuntu-latest 54 | steps: 55 | - uses: actions/checkout@v3 56 | - name: setup | rust 57 | uses: actions-rs/toolchain@v1 58 | with: 59 | toolchain: stable 60 | default: true 61 | profile: minimal 62 | - uses: Swatinem/rust-cache@v2 63 | - name: cargo tarpaulin 64 | uses: actions-rs/tarpaulin@v0.1 65 | with: 66 | version: '0.15.0' 67 | args: '--all-features -- --test-threads 1' 68 | - name: Upload to codecov.io 69 | uses: codecov/codecov-action@v3 70 | with: 71 | token: ${{ secrets.CODECOV_TOKEN }} 72 | files: cobertura.xml 73 | name: code-coverage-report 74 | verbose: true 75 | 76 | audit: 77 | name: security audit 78 | needs: check 79 | runs-on: ubuntu-latest 80 | steps: 81 | - uses: actions/checkout@v3 82 | - name: setup | rust 83 | uses: actions-rs/toolchain@v1 84 | with: 85 | toolchain: stable 86 | default: true 87 | profile: minimal 88 | - uses: Swatinem/rust-cache@v2 89 | - name: audit 90 | uses: actions-rs/audit-check@v1 91 | continue-on-error: true 92 | with: 93 | token: ${{ secrets.GITHUB_TOKEN }} 94 | 95 | publish-dry-run: 96 | name: publish dry run 97 | runs-on: ubuntu-latest 98 | steps: 99 | - uses: actions/checkout@v3 100 | - name: setup | rust 101 | uses: actions-rs/toolchain@v1 102 | with: 103 | toolchain: stable 104 | default: true 105 | profile: minimal 106 | - uses: Swatinem/rust-cache@v2 107 | - run: cargo publish --dry-run -p curlz 108 | 109 | docs: 110 | name: docs 111 | runs-on: ubuntu-latest 112 | steps: 113 | - uses: actions/checkout@v3 114 | - name: setup | rust 115 | uses: actions-rs/toolchain@v1 116 | with: 117 | toolchain: stable 118 | default: true 119 | profile: minimal 120 | - uses: Swatinem/rust-cache@v2 121 | - name: check documentation 122 | env: 123 | RUSTDOCFLAGS: -D warnings 124 | run: cargo doc --no-deps --all-features 125 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-ghcr.yml: -------------------------------------------------------------------------------- 1 | # see 2 | # https://docs.github.com/en/packages/managing-github-packages-using-github-actions-workflows/publishing-and-installing-a-package-with-github-actions 3 | name: publish docker image to ghcr.io 4 | 5 | on: 6 | workflow_call: 7 | workflow_dispatch: 8 | inputs: 9 | releaseVersion: 10 | description: version without 'v' to be released 11 | required: true 12 | type: string 13 | secrets: 14 | - GH_PAT: 15 | required: true 16 | release: 17 | types: 18 | - published 19 | 20 | jobs: 21 | build-and-push-image: 22 | runs-on: ubuntu-latest 23 | permissions: 24 | contents: read 25 | packages: write 26 | 27 | steps: 28 | - name: Checkout repository 29 | uses: actions/checkout@v3 30 | - name: Set up QEMU 31 | uses: docker/setup-qemu-action@v2 32 | - name: Set up Docker Buildx 33 | uses: docker/setup-buildx-action@v2 34 | - name: Build & Push Images 35 | shell: bash 36 | working-directory: docker 37 | env: 38 | LATEST_RELEASE_VERSION: ${{ inputs.releaseVersion }} 39 | LATEST_RELEASE_TAG: v${{ env.LATEST_RELEASE_VERSION }} 40 | GITHUB_TOKEN: ${{ secrets.GH_PAT }} 41 | # LATEST_RELEASE_VERSION: ${{ github.event.release.tag_name || inputs.releaseVersion }} 42 | run: | 43 | make login 44 | make build 45 | make test 46 | make multibuild 47 | make publish 48 | -------------------------------------------------------------------------------- /.github/workflows/release-assets.yml: -------------------------------------------------------------------------------- 1 | name: Release Binary Assets 2 | on: 3 | release: 4 | types: 5 | - published 6 | jobs: 7 | release: 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | include: 13 | - target: x86_64-unknown-linux-musl 14 | os: ubuntu-latest 15 | cross: true 16 | binName: curlz 17 | - target: x86_64-unknown-linux-gnu 18 | os: ubuntu-latest 19 | cross: true 20 | binName: curlz 21 | - target: aarch64-unknown-linux-musl 22 | os: ubuntu-latest 23 | cross: true 24 | binName: curlz 25 | - target: aarch64-unknown-linux-gnu 26 | os: ubuntu-latest 27 | cross: true 28 | binName: curlz 29 | - target: x86_64-apple-darwin 30 | os: macos-latest 31 | cross: false 32 | binName: curlz 33 | - target: aarch64-apple-darwin 34 | os: macos-latest 35 | cross: true 36 | binName: curlz 37 | - target: x86_64-pc-windows-msvc 38 | os: windows-latest 39 | cross: false 40 | binName: curlz.exe 41 | steps: 42 | - uses: actions/checkout@v3 43 | - name: Setup Rust 44 | uses: actions-rs/toolchain@v1 45 | with: 46 | toolchain: stable 47 | target: ${{ matrix.target }} 48 | override: true 49 | - uses: Swatinem/rust-cache@v2 50 | - name: Build 51 | uses: actions-rs/cargo@v1 52 | with: 53 | command: build 54 | use-cross: ${{ matrix.cross }} 55 | args: --all-features --release --target=${{ matrix.target }} 56 | - name: Create Archive 57 | id: archive 58 | shell: bash 59 | env: 60 | TARGET: ${{ matrix.target }} 61 | TAG: ${{ github.event.release.tag_name }} 62 | run: | 63 | filename="curlz-$TAG-$TARGET.tar.gz" 64 | tar -czvf "$filename" README.md LICENSE -C "target/$TARGET/release" "${{ matrix.binName }}" 65 | echo "::set-output name=filename::$filename" 66 | - name: Upload Archive 67 | uses: ncipollo/release-action@v1 68 | with: 69 | token: ${{ secrets.GH_PAT }} 70 | allowUpdates: true 71 | artifactErrorsFailBuild: true 72 | artifacts: ${{ steps.archive.outputs.filename }} 73 | artifactContentType: application/octet-stream 74 | omitBodyDuringUpdate: true 75 | omitNameDuringUpdate: true 76 | omitPrereleaseDuringUpdate: true 77 | -------------------------------------------------------------------------------- /.github/workflows/release-plz.yml: -------------------------------------------------------------------------------- 1 | name: Release PR+Tag+Publish 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | release-plz: 9 | name: Release it all 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v3 14 | with: 15 | fetch-depth: 0 16 | - name: Install Rust toolchain 17 | uses: dtolnay/rust-toolchain@stable 18 | - name: Run release-plz 19 | uses: MarcoIeni/release-plz-action@main 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GH_PAT }} 22 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # These are backup files generated by rustfmt 6 | **/*.rs.bk 7 | 8 | /.curlz/ 9 | .env 10 | -------------------------------------------------------------------------------- /.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Use `#` for headers 3 | MD003: 4 | style: atx 5 | 6 | # Set maximum line length 7 | MD013: 8 | line_length: 100 9 | 10 | # Use `---` for horizontal rule 11 | MD035: 12 | style: --- 13 | 14 | # Use ``` for code blocks 15 | MD046: 16 | style: fenced 17 | MD048: 18 | style: backtick 19 | 20 | # See https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md for 21 | # additional info 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.1.0-alpha.4](https://github.com/curlz-rs/curlz/compare/v0.1.0-alpha.3...v0.1.0-alpha.4) - 2023-01-16 10 | 11 | ### Added 12 | - *(http-language)* body variable substitution #21 (#24) 13 | - *(ci)* migrate to `release-plz` 14 | - *(http-language)* basics of the http language file format (#14) 15 | - *(functions)* implement `jwt` template function (#8) 16 | - *(essentials)* implement interactive prompt with a label (#3) 17 | - *(funding)* add the github sponsoring button 18 | - *(essentials)* `prompt_password()` special placeholder (#1) 19 | - *(essentials)* completing example 1 in the readme 20 | - *(essentials)* little refactoring 21 | - *(essentials)* switch template language to minijinja 22 | - *(essentials)* some more progress on basics 23 | - *(doc)* update the readme on features 24 | - *(essentials)* add first essential features 25 | - *(command:bookmark-as)* introduce insta testing 26 | - *(command:bookmark-as)* implement first `BookmarkAsCommand` 27 | - *(ci)* first build pipeline 28 | - remove dimensions from gif 29 | - add demo.gif 30 | - add first version of README.md 31 | - add first version of Cargo.toml 32 | 33 | ### Fixed 34 | - *(#10)* RUSTSEC-2020-0071: avoid full time featured time dependency (#11) 35 | - *(ci)* disable brew deployment for now 36 | - *(doc)* fix badges and repo links 37 | 38 | ### Other 39 | - *(ci)* release-please use the patch version bump strategy 40 | - *(ci)* release-please use the prerelease flag 41 | - *(ci)* fix release-please token variable 42 | - *(ci)* fix release-please add debug flag 43 | - *(ci)* fix release-please token issue again 44 | - *(ci)* fix release-please token issue 45 | - *(ci)* add release-please workflow 46 | - 0.1.0-alpha.3 47 | - add docs for placeholders at the beginning of urls (#9) 48 | - fix readme formatting issue 49 | - *(v0.1.0-alpha.2)* CHANGELOG + version bump + cargo update (#4) 50 | - `v0.1.0-alpha.1` (#2) 51 | - *(docs)* fix typos 52 | - *(doc)* fix cargo doc lint 53 | - *(fmt)* reformat 54 | - *(deps)* cargo update some deps 55 | - add todos for the next iteration 56 | - Initial commit 57 | - Initial commit 58 | # Changelog 59 | All notable changes to this project will be documented in this file. 60 | 61 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 62 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 63 | 64 | ## [Unreleased] - 2022-01-01 65 | [Unreleased]: https://github.com/curlz-rs/curlz/compare/v0.1.0-alpha.1...HEAD 66 | 67 | ### Added 68 | - special template function `jwt` for json web tokens [see #6](https://github.com/curlz-rs/curlz/issues/6) 69 | 70 | ### Changed 71 | ### Deprecated 72 | ### Removed 73 | ### Fixed 74 | ### Security 75 | ### Contributors 76 | - [@sassman](https://github.com/sassman) 77 | 78 | ## [0.1.0-alpha.2] - 2022-08-16 79 | [0.1.0-alpha.2]: https://github.com/curlz-rs/curlz/compare/v0.1.0-alpha.1..v0.1.0-alpha.2 80 | 81 | ### Added 82 | 83 | - special placeholder variables that would interact with the user 84 | - prompt for interactive input with a label as for example `{{ prompt_for("Username") }}` or `{{ prompt_for("Birthdate") }}` 85 | `curlz -- -u "{{ prompt_for("Username") }}:{{ prompt_password() }}" https://api.github.com/user` 86 | 87 | ### Contributors 88 | [@sassman](https://github.com/sassman) 89 | 90 | ## [0.1.0-alpha.1] - 2022-08-07 91 | [0.1.0-alpha.1]: https://github.com/curlz-rs/curlz/compare/v0.1.0-alpha.1 92 | 93 | ### Added 94 | - reading of `.env` files 95 | - reading of `.yaml` env files 96 | - placeholder evaluation, with the [minijinja](https://docs.rs/minijinja/latest/minijinja/) template engine 97 | - in urls 98 | - in http headers (`-H | --header` arguments) 99 | - in every other passed curl parameter 100 | - save request as a bookmark via `--bookmark` or `--bookmark-as`, containing: 101 | - curl arguments 102 | - http headers 103 | - http method 104 | - placeholders 105 | - pass all arguments after `--` to curl, that makes drop-in-replacement possible 106 | - execute a bookmarked request 107 | - special placeholder variables that would interact with the user 108 | - prompting for a password as `{{ prompt_password() }} 109 | ```sh 110 | curlz -- -u "{{ username }}:{{ prompt_password() }}" https://api.github.com/user 111 | ``` 112 | 113 | ### Contributors 114 | [@sassman](https://github.com/sassman) -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["curlz"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

curlz

4 | 5 | [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) 6 | [![Build Status](https://github.com/curlz-rs/curlz/workflows/Build/badge.svg)](https://github.com/curlz-rs/curlz/actions?query=branch%3Amain+workflow%3ABuild+) 7 | [![crates.io](https://img.shields.io/crates/v/curlz.svg)](https://crates.io/crates/curlz) 8 | [![dependency status](https://deps.rs/repo/github/curlz-rs/curlz/status.svg)](https://deps.rs/repo/github/curlz-rs/curlz) 9 | 10 |
11 | 12 | > a curl wrapper with placeholder, bookmark and environment powers just like postman but for the terminal 13 | 14 | ## Features 15 | 16 | - variables from `.env` and `.yaml` environment files 17 | - ️placeholder evaluation using the [minijinja](https://docs.rs/minijinja/latest/minijinja/) template engine, which can 18 | be used in URLs, HTTP headers, the HTTP body, and other passed curl parameters 19 | - ability to save requests as bookmarks and execute them by a shortname 20 | - support any curl argument after a `--`, that makes a drop-in-replacement for curl 21 | - special placeholders to interact on the terminal 22 | - prompt for a password 23 | as `{{ prompt_password() }}` [read more..](https://curlz-rs.github.io/curlz/template-functions.html#prompt-user-input---prompt_forname-string) 24 | - prompt for interactive input with a label 25 | as `{{ prompt_for("Username") }}` [read more..](https://curlz-rs.github.io/curlz/template-functions.html#prompt-user-for-password---prompt_password) 26 | - ️special placeholder for developers, 27 | like [Json Web Tokens (JWT)](https://curlz-rs.github.io/curlz/template-functions.html#json-web-token---jwtclaims-map-signing_key-string) 28 | or [Basic-Auth](https://curlz-rs.github.io/curlz/template-functions.html#basic-auth-token---basicusername-string-password-string) 29 | - send a http body via `-d | --data` or send json payload (with headers) via `--json` 30 | 31 | ## WIP 32 | 33 | - [⏳] support rest client template language [see #5](https://github.com/curlz-rs/curlz/issues/5) 34 | [check out the examples folder for more infos](./examples/http-file) 35 | 36 | ## Example #1 37 | 38 | In this example we're going to download a pre-configured `.gitignore` for a given language from GitHub via curl 39 | 40 | - `curl https://api.github.com/gitignore/templates/Rust` 41 | - the same with curlz: `curlz r https://api.github.com/gitignore/templates/Rust` 42 | - Add a placeholder that is interactively requested 43 | `curlz r 'https://api.github.com/gitignore/templates/{{ prompt_for("Language") | title }}'` 44 | - Now let's bookmark this request: 45 | ```sh 46 | curlz r --bookmark 'https://api.github.com/gitignore/templates/{{ prompt_for("Language") | title }}' 47 | Language: rust 48 | Please enter a bookmark name: gitignore 49 | ``` 50 | - Finally, we can keep using the bookmark from now on: `curlz r gitignore` 51 | 52 | ## Template function documentation 53 | 54 | Please 55 | read [the book to learn more about the template functions](https://curlz-rs.github.io/curlz/template-functions.html#template-function-documentation) 56 | -------------------------------------------------------------------------------- /curlz/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # These are backup files generated by rustfmt 6 | **/*.rs.bk 7 | 8 | /.curlz/ 9 | -------------------------------------------------------------------------------- /curlz/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.1.0-alpha.12](https://github.com/curlz-rs/curlz/compare/v0.1.0-alpha.11...v0.1.0-alpha.12) - 2023-03-24 10 | 11 | ### Added 12 | - robuster jwt token generation (#87) 13 | - *(cli)* implement `-u | --user` request cli argument (#7) (#46) 14 | - *(template)* implement the basic auth template function (#7) (#33) 15 | - *(template)* support environment variables from process / shell (#30) (#31) 16 | 17 | ### Fixed 18 | - *(http-lang)* make grammar bit more robust (#82) 19 | - *(deps)* update rust crate jsonwebtoken to 8.3 (#73) 20 | - *(deps)* update rust crate jsonwebtoken to 8.2 (#66) 21 | - *(deps)* update rust crate pest_derive to 2.5 (#68) 22 | - *(http-lang)* make grammar bit more robust for trailing whitespaces (#63) 23 | 24 | ### Other 25 | - *(deps)* update rust crate rstest to 0.17 (#84) 26 | - release v0.1.0-alpha.11 (#71) 27 | - *(deps)* update rust crate predicates to v3 (#72) 28 | - *(deps)* update rust crate tempfile to 3.4 (#65) 29 | - release v0.1.0-alpha.10 (#53) 30 | - migrate to clap v4 (#54) 31 | - *(deps)* bump pest from 2.5.5 to 2.5.6 (#51) 32 | - release 0.1.0-alpha.9 (#45) 33 | - release (#43) 34 | - release (#39) 35 | - release 0.1.0-alpha.6 (#34) 36 | - release `0.1.0-alpha.5` (#29) 37 | 38 | ## [0.1.0-alpha.11](https://github.com/curlz-rs/curlz/compare/v0.1.0-alpha.10...v0.1.0-alpha.11) - 2023-03-15 39 | 40 | ### Fixed 41 | - *(deps)* update rust crate jsonwebtoken to 8.3 (#73) 42 | - *(deps)* update rust crate jsonwebtoken to 8.2 (#66) 43 | - *(deps)* update rust crate pest_derive to 2.5 (#68) 44 | 45 | ### Other 46 | - *(deps)* update rust crate predicates to v3 (#72) 47 | - *(deps)* update rust crate tempfile to 3.4 (#65) 48 | - release v0.1.0-alpha.10 (#53) 49 | 50 | ## [0.1.0-alpha.10](https://github.com/curlz-rs/curlz/compare/v0.1.0-alpha.9...v0.1.0-alpha.10) - 2023-03-14 51 | 52 | ### Added 53 | - *(cli)* implement `-u | --user` request cli argument (#7) (#46) 54 | - *(template)* implement the basic auth template function (#7) (#33) 55 | - *(template)* support environment variables from process / shell (#30) (#31) 56 | 57 | ### Fixed 58 | - *(http-lang)* make grammar bit more robust for trailing whitespaces (#63) 59 | 60 | ### Other 61 | - migrate to clap v4 (#54) 62 | - *(deps)* bump pest from 2.5.5 to 2.5.6 (#51) 63 | - release 0.1.0-alpha.9 (#45) 64 | - release (#43) 65 | - release (#39) 66 | - release 0.1.0-alpha.6 (#34) 67 | - release `0.1.0-alpha.5` (#29) 68 | 69 | ## [0.1.0-alpha.9](https://github.com/curlz-rs/curlz/compare/v0.1.0-alpha.8...v0.1.0-alpha.9) - 2023-03-05 70 | 71 | ### Added 72 | - *(cli)* implement `-u | --user` request cli argument (#7) (#46) 73 | - *(template)* implement the basic auth template function (#7) (#33) 74 | - *(template)* support environment variables from process / shell (#30) (#31) 75 | 76 | ### Other 77 | - release (#43) 78 | - release (#39) 79 | - release 0.1.0-alpha.6 (#34) 80 | - release `0.1.0-alpha.5` (#29) 81 | 82 | ## [0.1.0-alpha.8](https://github.com/curlz-rs/curlz/compare/v0.1.0-alpha.7...v0.1.0-alpha.8) - 2023-03-05 83 | 84 | ### Added 85 | - *(template)* implement the basic auth template function (#7) (#33) 86 | - *(template)* support environment variables from process / shell (#30) (#31) 87 | 88 | ### Other 89 | - release (#39) 90 | - release 0.1.0-alpha.6 (#34) 91 | - release `0.1.0-alpha.5` (#29) 92 | 93 | ## [0.1.0-alpha.7](https://github.com/curlz-rs/curlz/compare/v0.1.0-alpha.6...v0.1.0-alpha.7) - 2023-03-03 94 | 95 | ### Added 96 | - *(template)* implement the basic auth template function (#7) (#33) 97 | - *(template)* support environment variables from process / shell (#30) (#31) 98 | 99 | ### Other 100 | - release 0.1.0-alpha.6 (#34) 101 | - release `0.1.0-alpha.5` (#29) 102 | 103 | ## [0.1.0-alpha.6](https://github.com/curlz-rs/curlz/compare/v0.1.0-alpha.5...v0.1.0-alpha.6) - 2023-03-02 104 | 105 | ### Added 106 | - *(template)* implement the basic auth template function (#7) (#33) 107 | - *(template)* support environment variables from process / shell (#30) (#31) 108 | 109 | ### Other 110 | - release `0.1.0-alpha.5` (#29) 111 | 112 | ## [0.1.0-alpha.5](https://github.com/curlz-rs/curlz/compare/v0.1.0-alpha.4...v0.1.0-alpha.5) - 2023-02-28 113 | 114 | ### Added 115 | - *(template)* support environment variables from process / shell (#30) (#31) 116 | -------------------------------------------------------------------------------- /curlz/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "curlz" 3 | authors = ["Sven Kanoldt "] 4 | description = "curl wrapper with placeholder, bookmark and environment powers just like postman" 5 | version = "0.1.0-alpha.12" 6 | edition = "2021" 7 | license = "GPL-3.0-only" 8 | include = ["src/**/*", "LICENSE", "*.md"] 9 | 10 | [dependencies] 11 | env_logger = "0.10" 12 | log = "0.4" 13 | clap = { version = "4.2", features = ["derive", "std", "cargo", "usage", "help"] } 14 | #clap_complete = "4.1" 15 | clap-verbosity-flag = "2.0" 16 | serde = { version = "1.0", features = ["derive"] } 17 | serde_yaml = "0.9" 18 | dotenvy = { version = "0.15" } 19 | anyhow = "1.0" 20 | convert_case = "0.6" 21 | dialoguer = "0.10" 22 | filenamify = "0.1" 23 | chrono = { version = "0.4", default-features = false, features = ["clock"] } 24 | humantime = "2.1" 25 | 26 | minijinja = "0.32" 27 | 28 | jsonwebtoken = "8.3" 29 | serde_json = { version = "1.0", features = ["preserve_order"] } 30 | base64 = "0.21" 31 | 32 | ## experimental 33 | pest = { version = "2.6", optional = true } 34 | pest_derive = { version = "2.6", optional = true } 35 | minijinja-stack-ref = "0.32" 36 | 37 | [features] 38 | "x-http-lang" = ['dep:pest', 'dep:pest_derive'] 39 | 40 | [dev-dependencies] 41 | insta = "1" 42 | tempfile = "3.5" 43 | assert_cmd = "2.0" 44 | predicates = "3.0" 45 | rstest = "0.17" 46 | wiremock = "0.5" 47 | tokio = { version = "1", features = ["rt", "macros"], default-features = false } 48 | indoc = "2" 49 | pretty_assertions = "1" 50 | 51 | [[bin]] 52 | name = "curlz" 53 | path = "src/bin/curlz.rs" 54 | 55 | [lib] 56 | name = "curlz" 57 | path = "src/curlz/lib.rs" 58 | -------------------------------------------------------------------------------- /curlz/LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /curlz/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

curlz

4 | 5 | [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) 6 | [![Build Status](https://github.com/curlz-rs/curlz/workflows/Build/badge.svg)](https://github.com/curlz-rs/curlz/actions?query=branch%3Amain+workflow%3ABuild+) 7 | [![crates.io](https://img.shields.io/crates/v/curlz.svg)](https://crates.io/crates/curlz) 8 | [![dependency status](https://deps.rs/repo/github/curlz-rs/curlz/status.svg)](https://deps.rs/repo/github/curlz-rs/curlz) 9 | 10 |
11 | 12 | > a curl wrapper with placeholder, bookmark and environment powers just like postman but for the terminal 13 | 14 | ## Features 15 | 16 | - variables from `.env` and `.yaml` environment files 17 | - ️placeholder evaluation using the [minijinja](https://docs.rs/minijinja/latest/minijinja/) template engine, which can be used in URLs, HTTP headers, the HTTP body, and other passed curl parameters 18 | - ability to save and execute requests as bookmarks with a shortname 19 | - support any curl argument after a `--`, that makes a drop-in-replacement for curl 20 | - special placeholders to interact with the user 21 | - prompt for a password as `{{ prompt_password() }}` 22 | `curlz r https://api.github.com/user -- -u "{{ username }}:{{ prompt_password() }}"` 23 | - prompt for interactive input with a label as `{{ prompt_for("Username") }}` or `{{ prompt_for("Birthdate") }}` 24 | `curlz -- -u "{{ prompt_for("Username") }}:{{ prompt_password() }}" https://api.github.com/user` 25 | - ️evaluate placeholders at the beginning of an url like: 26 | `curlz r --define 'host=https://httpbin.org' '{{host}}/get'` 27 | - ️special placeholder for developers, like for Json Web Tokens (JWT) 28 | `{{ jwt(claims, jwt_signing_key) }}`, where `claims` and `jwt_signing_key` are looked up at the environment file or can be directly provided map and string 29 | `curlz r -H 'Authorization: Bearer {{ jwt({"uid": "1234"}, "000") }}' https://httpbin.org/bearer -- -vvv` 30 | - send a http body via `-d | --data` 31 | `curlz r -d 'Hello World' -X POST https://httpbin.org/anything` 32 | - send a json payload and headers with the `--json` argument 33 | `curlz r --json '{ "foo": "bar" }' -X POST 'https://httpbin.org/anything'` 34 | 35 | ## WIP 36 | - [⏳] support rest client template language [see #5](https://github.com/curlz-rs/curlz/issues/5) 37 | 38 | ## Example #1 39 | 40 | In this example we're going to download a pre-configured `.gitignore` for a given language from GitHub via curl 41 | 42 | - `curl https://api.github.com/gitignore/templates/Rust` 43 | - the same with curlz: `curlz r https://api.github.com/gitignore/templates/Rust` 44 | - Add a placeholder that is interactively requested 45 | `curlz r 'https://api.github.com/gitignore/templates/{{ prompt_for("Language") | title }}'` 46 | - Now let's bookmark this request: 47 | ```sh 48 | curlz r --bookmark 'https://api.github.com/gitignore/templates/{{ prompt_for("Language") | title }}' 49 | Language: rust 50 | Please enter a bookmark name: gitignore 51 | ``` 52 | - Finally, we can keep using the bookmark from now on: `curlz r gitignore` 53 | 54 | ## Template function documentation 55 | 56 | ### Json Web Token - `jwt(claims: map, [jwt_signing_key: string])` 57 | - arguments: 58 | - `claims`: to be a map of key value pairs like `{"uid": "1234"}` that are the payload of the JWT 59 | - `jwt_signing_key`: to be a string, this is optional and can be provided at the environment file with a variable named `jwt_signing_key` 60 | - output: string is a Json Web Token (JWT) 61 | - notes: 62 | - the hash algorithm is `HS256` and the JWT header is `{"alg": "HS256", "typ": "JWT"}` 63 | - the claim `exp` expiry time is set to in 15min by default, but can be overwritten 64 | - the claim `iat` issued at timestamp is set automatically and cannot be overwritten 65 | -------------------------------------------------------------------------------- /curlz/src/bin/curlz.rs: -------------------------------------------------------------------------------- 1 | pub fn main() -> curlz::Result<()> { 2 | curlz::cli::execute() 3 | } 4 | -------------------------------------------------------------------------------- /curlz/src/curlz/cli/execute.rs: -------------------------------------------------------------------------------- 1 | use super::sub_commands::*; 2 | 3 | use clap::Parser; 4 | use clap_verbosity_flag::{InfoLevel, Verbosity}; 5 | use env_logger::Target; 6 | 7 | #[derive(Clone, Debug, Parser)] 8 | #[command(author, version, about, long_about = None )] 9 | #[clap(subcommand_required = true, arg_required_else_help = true)] 10 | #[command(propagate_version = true)] 11 | #[command(name = "curlz")] 12 | pub struct Cli { 13 | #[command(flatten)] 14 | verbose: Verbosity, 15 | #[command(subcommand)] 16 | pub command: SubCommands, 17 | } 18 | 19 | pub fn execute() -> crate::Result<()> { 20 | let args = Cli::parse(); 21 | env_logger::Builder::new() 22 | .filter_level(args.verbose.log_level_filter()) 23 | .target(Target::Stderr) 24 | .init(); 25 | 26 | match args.command { 27 | SubCommands::Request(ref r) => r.execute(), 28 | SubCommands::Bookmark(_b) => { 29 | todo!() 30 | } 31 | #[cfg(feature = "x-http-lang")] 32 | SubCommands::HttpFile(ref hf) => hf.execute(), 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /curlz/src/curlz/cli/header_args.rs: -------------------------------------------------------------------------------- 1 | use crate::domain::http::HttpHeaders; 2 | 3 | pub struct HeaderArgs(Vec); 4 | 5 | impl AsRef<[String]> for HeaderArgs { 6 | fn as_ref(&self) -> &[String] { 7 | self.0.as_slice() 8 | } 9 | } 10 | 11 | impl From<&Vec> for HeaderArgs { 12 | fn from(raw_headers: &Vec) -> Self { 13 | let mut headers = vec![]; 14 | let mut i = 0_usize; 15 | while i < raw_headers.len() { 16 | match raw_headers.get(i).unwrap().as_str() { 17 | "-H" | "--header" => { 18 | headers.push(raw_headers.get(i + 1).unwrap().to_string()); 19 | i += 2; 20 | } 21 | _ => { 22 | i += 1; 23 | } 24 | } 25 | } 26 | 27 | HeaderArgs(headers) 28 | } 29 | } 30 | 31 | impl From for HttpHeaders { 32 | fn from(raw_headers: HeaderArgs) -> Self { 33 | raw_headers.as_ref().into() 34 | } 35 | } 36 | 37 | #[cfg(test)] 38 | mod tests { 39 | use super::*; 40 | 41 | #[test] 42 | fn should_parse_the_header_arg_flag_away() { 43 | let mut args = vec![ 44 | "-H", 45 | "foo: bar", 46 | "--header", 47 | "Accept: application/json", 48 | "--header", 49 | r#"Authorization: Baerer {{ jwt({"foo": "bar"}) }}"#, 50 | "http://example.com", 51 | ] 52 | .iter() 53 | .map(|s| s.to_string()) 54 | .collect::>(); 55 | 56 | let headers: HeaderArgs = (&args).into(); 57 | 58 | assert_eq!( 59 | headers.as_ref(), 60 | &[ 61 | "foo: bar".to_string(), 62 | "Accept: application/json".to_string(), 63 | r#"Authorization: Baerer {{ jwt({"foo": "bar"}) }}"#.to_string() 64 | ] 65 | ); 66 | assert_eq!(args.len(), 7); 67 | assert_eq!(args.pop(), Some("http://example.com".to_string())); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /curlz/src/curlz/cli/interactive.rs: -------------------------------------------------------------------------------- 1 | use crate::Result; 2 | 3 | use dialoguer::Input; 4 | 5 | pub fn user_question(prompt: &str, default: &Option) -> Result { 6 | let mut i = Input::::new(); 7 | i.with_prompt(prompt.to_string()); 8 | if let Some(s) = default { 9 | i.default(s.to_owned()); 10 | } 11 | i.interact().map_err(Into::::into) 12 | } 13 | -------------------------------------------------------------------------------- /curlz/src/curlz/cli/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod execute; 2 | mod header_args; 3 | pub mod interactive; 4 | pub mod sub_commands; 5 | 6 | pub use execute::execute; 7 | pub use header_args::HeaderArgs; 8 | pub use sub_commands::*; 9 | -------------------------------------------------------------------------------- /curlz/src/curlz/cli/sub_commands/bookmark.rs: -------------------------------------------------------------------------------- 1 | use clap::{Args, Subcommand}; 2 | 3 | #[derive(Clone, Debug, Args)] 4 | #[command(arg_required_else_help = true)] 5 | #[command(args_conflicts_with_subcommands = true)] 6 | pub struct BookmarkCli { 7 | #[clap(subcommand)] 8 | pub command: BookmarkCommands, 9 | } 10 | 11 | #[derive(Clone, Debug, Subcommand)] 12 | pub enum BookmarkCommands { 13 | List, 14 | Rename { 15 | #[clap(value_parser)] 16 | name: String, 17 | #[clap(value_parser)] 18 | new_name: String, 19 | }, 20 | Remove { 21 | #[clap(value_parser)] 22 | name: String, 23 | }, 24 | Show { 25 | #[clap(value_parser)] 26 | name: String, 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /curlz/src/curlz/cli/sub_commands/http_file.rs: -------------------------------------------------------------------------------- 1 | use crate::domain::environment::create_environment; 2 | use crate::domain::http_lang::parse_request_file; 3 | use crate::domain::request::Verbosity::Verbose; 4 | use crate::domain::request::{issue_request_with_curl, IssueRequest}; 5 | use crate::template::variables::Placeholder; 6 | use crate::utils::parse_pairs; 7 | use clap::Parser; 8 | use std::path::PathBuf; 9 | 10 | #[derive(Clone, Debug, Parser)] 11 | #[clap(author, version, about, long_about = None)] 12 | #[command(arg_required_else_help = true)] 13 | pub struct HttpFileCli { 14 | /// Provide an `.env` or a yaml containing template variables 15 | #[clap(long = "env-file", value_parser, default_value = ".env")] 16 | pub env_file: PathBuf, 17 | 18 | /// Define a adhoc template variable like `--define foo=value --define bar=42`, see also `--env-file` for more convenience 19 | #[clap(long, number_of_values = 1, value_parser)] 20 | pub define: Vec, 21 | 22 | /// Provide an http request file 23 | #[clap(value_parser)] 24 | pub http_file: PathBuf, 25 | } 26 | 27 | impl HttpFileCli { 28 | pub fn execute(&self) -> crate::Result<()> { 29 | let placeholders: Vec = self 30 | .define 31 | .iter() 32 | .map(|kv| parse_pairs(kv, '=')) 33 | .filter(Option::is_some) 34 | .flatten() 35 | .map(|(key, value)| Placeholder::new(key, value)) 36 | .collect(); 37 | let env = create_environment(&self.env_file, &placeholders)?; 38 | let contents = std::fs::read_to_string(&self.http_file)?; 39 | let bookmarks = parse_request_file(contents)?; 40 | 41 | for b in bookmarks { 42 | issue_request_with_curl(IssueRequest::new(&b.request, Verbose), &env)?; 43 | } 44 | 45 | Ok(()) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /curlz/src/curlz/cli/sub_commands/mod.rs: -------------------------------------------------------------------------------- 1 | use clap::Subcommand; 2 | 3 | mod bookmark; 4 | mod request; 5 | 6 | pub use bookmark::*; 7 | pub use request::*; 8 | 9 | #[cfg(feature = "x-http-lang")] 10 | mod http_file; 11 | #[cfg(feature = "x-http-lang")] 12 | pub use http_file::*; 13 | 14 | #[derive(Clone, Debug, Subcommand)] 15 | pub enum SubCommands { 16 | #[command(alias("r"))] 17 | Request(RequestCli), 18 | #[command(alias("b"))] 19 | /// similar to git remote, we want to support `list`, `add`, `rename`, `remove` and `show` 20 | Bookmark(BookmarkCli), 21 | #[cfg(feature = "x-http-lang")] 22 | HttpFile(HttpFileCli), 23 | } 24 | -------------------------------------------------------------------------------- /curlz/src/curlz/cli/sub_commands/request.rs: -------------------------------------------------------------------------------- 1 | use crate::cli::interactive; 2 | use crate::cli::HeaderArgs; 3 | use crate::domain::bookmark::{ 4 | load_bookmark, save_bookmark, BookmarkCollection, BookmarkFolderCollection, LoadBookmark, 5 | SaveBookmark, 6 | }; 7 | use crate::domain::http::{ 8 | HttpBody, HttpHeaders, HttpMethod, HttpRequest, HttpUri, HttpVersion::Http11, 9 | }; 10 | use crate::domain::request::Verbosity::{Silent, Verbose}; 11 | use crate::domain::request::{issue_request_with_curl, IssueRequest}; 12 | use crate::template::variables::Placeholder; 13 | use crate::utils::parse_pairs; 14 | 15 | use crate::domain::environment::create_environment; 16 | use anyhow::{bail, Context}; 17 | use clap::Parser; 18 | use clap_verbosity_flag::{InfoLevel, Verbosity}; 19 | use log::info; 20 | use std::path::PathBuf; 21 | use std::str::FromStr; 22 | 23 | #[derive(Clone, Debug, Parser)] 24 | #[command(author, version, about, long_about = None)] 25 | #[clap(arg_required_else_help = true)] 26 | pub struct RequestCli { 27 | #[clap(flatten)] 28 | pub verbose: Verbosity, 29 | 30 | #[clap(long = "bookmark-as", value_parser)] 31 | pub save_bookmark_as: Option, 32 | 33 | #[clap(long = "bookmark", action)] 34 | pub save_bookmark: bool, 35 | 36 | /// Provide an `.env` or a yaml containing template variables 37 | #[clap(long = "env-file", value_parser, default_value = ".env")] 38 | pub env_file: PathBuf, 39 | 40 | /// Define a adhoc template variable like `--define foo=value --define bar=42`, see also `--env-file` for more convenience 41 | #[clap(long, number_of_values = 1, value_parser)] 42 | pub define: Vec, 43 | 44 | #[clap(short = 'X', long = "request", value_parser, default_value = "GET")] 45 | pub http_method: String, 46 | 47 | /// set one ore more http headers in the form of `"Header-Name: Value"` 48 | /// 49 | /// ## Examples 50 | /// 51 | /// - `curlz -H "X-First-Name: Joe" https://example.com` 52 | /// 53 | /// - `curlz -H "User-Agent: yes-please/2000" https://example.com` 54 | /// 55 | /// - `curlz -H "Host:" https://example.com` 56 | #[clap(long = "header", short = 'H', value_parser)] 57 | pub headers: Vec, 58 | 59 | /// set a http body 60 | #[clap(short = 'd', long = "data", value_parser)] 61 | pub http_payload: Option, 62 | 63 | /// this is a lazy shortcut for setting 2 headers and a http body 64 | /// 65 | /// `curlz -H "Content-Type: application/json" -H "Accept: application/json" --data ` 66 | #[clap(long = "json", value_parser)] 67 | pub json: Option, 68 | 69 | /// 70 | /// Specify the user name and password to use for server authentication. 71 | /// 72 | /// Note: in cases where only the user is provided, 73 | /// curlz will prompt for the password interactively 74 | /// 75 | /// Equivalent to: `curlz -H 'Authorization: Basic {{ basic("user", "password") }}'` 76 | #[clap(short = 'u', long = "user", value_parser)] 77 | pub user: Option, 78 | 79 | #[clap(value_parser)] 80 | pub bookmark_or_url: Option, 81 | 82 | #[clap(value_parser, last = true)] 83 | pub raw: Vec, 84 | } 85 | 86 | impl RequestCli { 87 | pub fn parse_define_as_placeholders(&self) -> Vec { 88 | self.define 89 | .iter() 90 | .map(|kv| parse_define(kv.as_str())) 91 | .filter(Option::is_some) 92 | .flatten() 93 | .map(|(key, value)| Placeholder::new(key, value)) 94 | .collect() 95 | } 96 | } 97 | 98 | /// parses `key=value` strings into tuples of (key, value) 99 | #[inline] 100 | fn parse_define(define: &str) -> Option<(&str, &str)> { 101 | parse_pairs(define, '=') 102 | } 103 | 104 | impl RequestCli { 105 | pub fn execute(&self) -> crate::Result<()> { 106 | let placeholders = self.parse_define_as_placeholders(); 107 | let env = create_environment(&self.env_file, &placeholders)?; 108 | let mut raw = self.raw.clone(); 109 | 110 | let method = extract_method(&mut raw) 111 | .unwrap_or_else(|| HttpMethod::from_str(self.http_method.as_str()))?; 112 | 113 | // headers 114 | let mut headers: HttpHeaders = self.headers.as_slice().into(); 115 | let (mut raw, headers_args) = extract_headers(&raw); 116 | let headers_raw: HttpHeaders = headers_args.into(); 117 | headers.merge(&headers_raw); 118 | if self.json.is_some() { 119 | headers.push("Content-Type", "application/json"); 120 | headers.push("Accept", "application/json"); 121 | } 122 | if self.user.is_some() { 123 | parse_user_to_header(self.user.as_ref().unwrap(), &mut headers)?; 124 | } 125 | 126 | let body = self 127 | .http_payload 128 | .as_ref() 129 | .map(|b| HttpBody::InlineText(b.to_string())) 130 | .or_else(|| self.json.clone().map(HttpBody::InlineText)) 131 | .unwrap_or_default(); 132 | 133 | let request = if let Some(bookmark_or_url) = self.bookmark_or_url.as_ref() { 134 | if is_url(bookmark_or_url) { 135 | // here we are certain we got an URL 136 | HttpRequest { 137 | // todo: also replace placeholders in there.. 138 | url: bookmark_or_url.to_string().try_into()?, 139 | method, 140 | version: Http11, 141 | headers, 142 | body, 143 | placeholders, 144 | // todo: implement placeholder scanning.. 145 | curl_params: raw, 146 | } 147 | } else { 148 | let bookmark_collection = bookmark_collection()?; 149 | // here we might have a bookmark slug, but not sure yet 150 | let bookmark = load_bookmark( 151 | LoadBookmark::new(bookmark_or_url, method), 152 | &bookmark_collection, 153 | ) 154 | .context("No Bookmark with the given name found")?; 155 | 156 | bookmark.request().update(|request| { 157 | request.headers.merge(&headers); 158 | request.curl_params.extend_from_slice(&raw); 159 | }) 160 | } 161 | } else { 162 | // try to extract an URL from the raw args provided 163 | extract_url(&mut raw) 164 | .context("Raw arguments did not contain any URL") 165 | .map(|url| HttpRequest { 166 | url, 167 | method, 168 | version: Http11, 169 | headers, 170 | body, 171 | placeholders, 172 | // todo: implement placeholder scanning.. 173 | curl_params: raw, 174 | })? 175 | }; 176 | 177 | issue_request_with_curl( 178 | IssueRequest::new( 179 | &request, 180 | if self.verbose.is_silent() { 181 | Silent 182 | } else { 183 | Verbose 184 | }, 185 | ), 186 | &env, 187 | )?; 188 | 189 | if self.save_bookmark || self.save_bookmark_as.is_some() { 190 | let slug = if let Some(answer) = self.save_bookmark_as.as_ref() { 191 | answer.clone() 192 | } else { 193 | interactive::user_question("Please enter a bookmark name", &None)? 194 | }; 195 | 196 | let mut bookmark_collection = bookmark_collection()?; 197 | save_bookmark( 198 | SaveBookmark::new(slug.as_str(), &request), 199 | &mut bookmark_collection, 200 | )?; 201 | 202 | info!("Request bookmarked as: {}", slug); 203 | } 204 | 205 | Ok(()) 206 | } 207 | } 208 | 209 | fn parse_user_to_header(user: &str, headers: &mut HttpHeaders) -> crate::Result<()> { 210 | let user_pw: Vec<&str> = user.split_terminator(':').collect(); 211 | let header_value = match user_pw.len() { 212 | 1 => { 213 | format!( 214 | r#"Basic {{{{ basic("{}", prompt_password()) }}}}"#, 215 | user_pw.first().unwrap() 216 | ) 217 | } 218 | 2 => { 219 | format!( 220 | r#"Basic {{{{ basic("{}", "{}") }}}}"#, 221 | user_pw.first().unwrap(), 222 | user_pw.get(1).unwrap() 223 | ) 224 | } 225 | _ => bail!("-u | -user argument was invalid"), 226 | }; 227 | headers.push("Authorization", header_value); 228 | 229 | Ok(()) 230 | } 231 | 232 | fn bookmark_collection() -> crate::Result { 233 | BookmarkFolderCollection::new() 234 | } 235 | 236 | /// checks if a string is a URL 237 | fn is_url(potential_url: impl AsRef) -> bool { 238 | let trimmed_url = potential_url.as_ref().trim_start_matches('\''); 239 | 240 | trimmed_url.starts_with("http") || trimmed_url.starts_with("{{") 241 | } 242 | 243 | /// Extracts the http headers from command line arguments 244 | /// If a header `-H | --header` is found, it's removed from the `raw_args` 245 | fn extract_headers(raw_args: &Vec) -> (Vec, HeaderArgs) { 246 | let headers = HeaderArgs::from(raw_args); 247 | 248 | let mut non_header_args = vec![]; 249 | let mut i = 0_usize; 250 | while i < raw_args.len() { 251 | match raw_args.get(i).unwrap().as_str() { 252 | "-H" | "--header" => { 253 | i += 2; 254 | } 255 | v => { 256 | non_header_args.push(v.to_string()); 257 | i += 1; 258 | } 259 | } 260 | } 261 | 262 | (non_header_args, headers) 263 | } 264 | 265 | /// Extracts the http method from the command line arguments 266 | /// If a method `-X | --request` is found, it's removed from the `raw_args` 267 | /// 268 | /// ## Fallible 269 | /// If the method is provided but invalid, an error is returned 270 | /// 271 | /// ## None 272 | /// simple: in case no http method is provided, None is returned 273 | fn extract_method(raw_args: &mut Vec) -> Option> { 274 | let mut method = None; 275 | 276 | let copy = raw_args.clone(); 277 | copy.iter() 278 | .enumerate() 279 | .step_by(2) 280 | .zip(copy.iter().enumerate().skip(1).step_by(2)) 281 | .for_each(|((ik, key), (iv, value))| { 282 | if key.as_str().eq("-X") || key.as_str().eq("--request") { 283 | raw_args.remove(ik); 284 | raw_args.remove(iv); 285 | method = Some(HttpMethod::from_str(value.as_str())); 286 | } 287 | }); 288 | 289 | method 290 | } 291 | 292 | /// extracts a `http://` or `https://` URL from the command line arguments `raw_args` 293 | /// if a URL is found it's removed from the `raw_args` vector and returned 294 | /// If no URL is found, returns `None` 295 | fn extract_url(raw_args: &mut Vec) -> Option { 296 | if let Some(potential_url) = raw_args.last().cloned() { 297 | if potential_url.trim_start_matches('\'').starts_with("http") { 298 | raw_args.pop(); 299 | // todo: no unwrap here: 300 | Some(potential_url.try_into().unwrap()) 301 | } else if potential_url.starts_with("{{") { 302 | todo!("placeholder evaluation at the beginning of URLs") 303 | } else { 304 | None 305 | } 306 | } else { 307 | None 308 | } 309 | } 310 | 311 | #[cfg(test)] 312 | mod tests { 313 | use super::*; 314 | 315 | #[test] 316 | fn should_split_defines_by_equal() { 317 | assert_eq!(parse_define("foo=bar"), Some(("foo", "bar"))); 318 | assert_eq!(parse_define("foo"), None); 319 | assert_eq!(parse_define("baz="), Some(("baz", ""))); 320 | } 321 | 322 | #[test] 323 | fn should_extract_a_url_as_last_argument() { 324 | let mut args = vec!["--request", "GET", "http://example.com"] 325 | .iter() 326 | .map(|s| s.to_string()) 327 | .collect(); 328 | let url = extract_url(&mut args); 329 | assert_eq!( 330 | url, 331 | Some("http://example.com".to_string().try_into().unwrap()) 332 | ); 333 | assert_eq!(args.len(), 2); 334 | } 335 | 336 | #[test] 337 | fn should_extract_method() { 338 | let mut args = vec!["--request", "GET", "http://example.com"] 339 | .iter() 340 | .map(|s| s.to_string()) 341 | .collect(); 342 | let method = extract_method(&mut args).unwrap().unwrap(); 343 | assert_eq!(method, HttpMethod::Get); 344 | assert_eq!(args.len(), 1); 345 | } 346 | 347 | #[test] 348 | fn should_extract_headers() { 349 | let args = vec![ 350 | "-vvv", 351 | "-H", 352 | "foo: bar", 353 | "--header", 354 | "Accept: application/json", 355 | "--header", 356 | r#"Authorization: Baerer {{ jwt({"foo": "bar"}) }}"#, 357 | "http://example.com", 358 | ] 359 | .iter() 360 | .map(ToString::to_string) 361 | .collect(); 362 | let (args, headers) = extract_headers(&args); 363 | assert_eq!( 364 | headers.as_ref(), 365 | &[ 366 | "foo: bar".to_string(), 367 | "Accept: application/json".to_string(), 368 | r#"Authorization: Baerer {{ jwt({"foo": "bar"}) }}"#.to_string(), 369 | ] 370 | ); 371 | // TODO: it's unclear why this here fails: 372 | assert_eq!(args.len(), 2); 373 | assert_eq!(args.first(), Some(&"-vvv".to_string())); 374 | assert_eq!(args.last(), Some(&"http://example.com".to_string())); 375 | } 376 | 377 | #[test] 378 | fn should_parse_user_to_header() { 379 | let mut headers = HttpHeaders::default(); 380 | parse_user_to_header("john:secret", &mut headers).unwrap(); 381 | assert_eq!( 382 | headers.get("Authorization").unwrap(), 383 | r#"Basic {{ basic("john", "secret") }}"# 384 | ) 385 | } 386 | #[test] 387 | fn should_parse_user_to_header_without_password() { 388 | let mut headers = HttpHeaders::default(); 389 | parse_user_to_header("john", &mut headers).unwrap(); 390 | assert_eq!( 391 | headers.get("Authorization").unwrap(), 392 | r#"Basic {{ basic("john", prompt_password()) }}"# 393 | ) 394 | } 395 | 396 | #[test] 397 | #[should_panic(expected = "-u | -user argument was invalid")] 398 | fn should_bail_on_garbage_in() { 399 | let mut headers = HttpHeaders::default(); 400 | parse_user_to_header("john:xxxx:asdfa", &mut headers).unwrap(); 401 | } 402 | } 403 | -------------------------------------------------------------------------------- /curlz/src/curlz/domain/bookmark/collection.rs: -------------------------------------------------------------------------------- 1 | use crate::domain::bookmark::Bookmark; 2 | use crate::domain::http::HttpMethod; 3 | use crate::Result; 4 | 5 | pub trait BookmarkCollection { 6 | fn save(&self, bookmark: &Bookmark) -> Result<()>; 7 | fn load(&self, name: impl AsRef, method: &HttpMethod) -> Result>; 8 | } 9 | -------------------------------------------------------------------------------- /curlz/src/curlz/domain/bookmark/collection_impl/bookmark_folder_collection.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow; 2 | use convert_case::{Case, Casing}; 3 | use filenamify::filenamify; 4 | use std::path::PathBuf; 5 | use std::{env, fs}; 6 | 7 | use crate::domain::bookmark::collection::BookmarkCollection; 8 | use crate::domain::bookmark::Bookmark; 9 | use crate::domain::http::HttpMethod; 10 | use crate::Result; 11 | 12 | const WORKSPACE_FOLDER: &str = ".curlz"; 13 | const BOOKMARK_FOLDER: &str = "bookmarks"; 14 | 15 | pub struct BookmarkFolderCollection { 16 | working_dir: PathBuf, 17 | } 18 | 19 | impl BookmarkFolderCollection { 20 | pub fn new() -> Result { 21 | Ok(Self { 22 | working_dir: env::current_dir() 23 | .map_err(|e| anyhow!("cannot create processor: {}", e))?, 24 | }) 25 | } 26 | } 27 | 28 | impl BookmarkCollection for BookmarkFolderCollection { 29 | fn save(&self, bookmark: &Bookmark) -> Result<()> { 30 | let slug = bookmark.slug(); 31 | let request = bookmark.request(); 32 | 33 | let file_name = filenamify(format!("{:?} {}", &request.method, slug)).to_case(Case::Snake); 34 | let bookmark = serde_yaml::to_string(&bookmark)?; 35 | 36 | let bookmarks_path = self 37 | .working_dir 38 | .join(WORKSPACE_FOLDER) 39 | .join(BOOKMARK_FOLDER); 40 | fs::create_dir_all(bookmarks_path.as_path())?; 41 | { 42 | fs::write( 43 | bookmarks_path.join(format!("{}.yml", file_name.as_str())), 44 | bookmark, 45 | ) 46 | .map_err(|e| anyhow!("cannot write request bookmark to file: {}", e)) 47 | } 48 | } 49 | 50 | fn load(&self, name: impl AsRef, method: &HttpMethod) -> Result> { 51 | let slug = name.as_ref(); 52 | let bookmarks_path = self 53 | .working_dir 54 | .join(WORKSPACE_FOLDER) 55 | .join(BOOKMARK_FOLDER); 56 | let file_name = filenamify(format!("{:?} {}", method, slug)).to_case(Case::Snake); 57 | let file_path = bookmarks_path.join(format!("{}.yml", file_name.as_str())); 58 | if !file_path.exists() { 59 | return Ok(None); 60 | } 61 | let bookmark = fs::read_to_string(file_path)?; 62 | Ok(Some(serde_yaml::from_str(&bookmark)?)) 63 | } 64 | } 65 | 66 | #[cfg(test)] 67 | mod tests { 68 | use super::*; 69 | use crate::domain::bookmark::SaveBookmark; 70 | use crate::domain::http::HttpVersion::Http11; 71 | use crate::domain::http::{HttpBody, HttpMethod}; 72 | use crate::domain::http::{HttpHeaders, HttpRequest}; 73 | use crate::template::variables::Placeholder; 74 | use tempfile::{tempdir, TempDir}; 75 | 76 | impl BookmarkFolderCollection { 77 | pub fn temporary() -> (Self, TempDir) { 78 | let tempdir = tempdir().unwrap(); 79 | ( 80 | Self { 81 | working_dir: tempdir.path().to_path_buf(), 82 | }, 83 | tempdir, 84 | ) 85 | } 86 | } 87 | 88 | #[test] 89 | fn should_handle_save_bookmark_command() { 90 | let request = HttpRequest { 91 | url: "{{protonmail_api_baseurl}}/pks/lookup?op=get&search={{email}}".into(), 92 | method: HttpMethod::Get, 93 | version: Http11, 94 | headers: HttpHeaders::default(), 95 | body: HttpBody::default(), 96 | curl_params: vec![], 97 | placeholders: vec![email_placeholder(), protonmail_api_baseurl_placeholder()], 98 | }; 99 | let cmd = SaveBookmark::new("/protonmail/gpg/:email", &request); 100 | 101 | let (p, tmp) = BookmarkFolderCollection::temporary(); 102 | p.save(&(&cmd).into()).unwrap(); 103 | 104 | let saved_bookmark = String::from_utf8( 105 | fs::read( 106 | tmp.path() 107 | .join(WORKSPACE_FOLDER) 108 | .join(BOOKMARK_FOLDER) 109 | .join("get_protonmail_gpg_email.yml"), 110 | ) 111 | .unwrap(), 112 | ) 113 | .unwrap(); 114 | 115 | insta::assert_snapshot!(saved_bookmark); 116 | } 117 | 118 | fn email_placeholder() -> Placeholder { 119 | Placeholder { 120 | name: "email".to_string(), 121 | value: None, 122 | default: None, 123 | prompt: "enter an email address".to_string().into(), 124 | } 125 | } 126 | 127 | fn protonmail_api_baseurl_placeholder() -> Placeholder { 128 | Placeholder { 129 | name: "protonmail_api_baseurl".to_string(), 130 | value: None, 131 | default: "https://api.protonmail.ch".to_string().into(), 132 | prompt: "enter the protonmail api baseurl".to_string().into(), 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /curlz/src/curlz/domain/bookmark/collection_impl/mod.rs: -------------------------------------------------------------------------------- 1 | mod bookmark_folder_collection; 2 | 3 | pub use bookmark_folder_collection::BookmarkFolderCollection; 4 | -------------------------------------------------------------------------------- /curlz/src/curlz/domain/bookmark/collection_impl/snapshots/curlz__domain__bookmark__collection_impl__bookmark_folder_collection__tests__should_handle_save_bookmark_command.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: curlz/src/curlz/domain/bookmark/collection_impl/bookmark_folder_collection.rs 3 | expression: saved_bookmark 4 | --- 5 | slug: /protonmail/gpg/:email 6 | request: 7 | url: '{{protonmail_api_baseurl}}/pks/lookup?op=get&search={{email}}' 8 | method: GET 9 | version: HTTP/1.1 10 | headers: [] 11 | body: None 12 | curl_params: [] 13 | placeholders: 14 | - name: email 15 | value: null 16 | default: null 17 | prompt: enter an email address 18 | - name: protonmail_api_baseurl 19 | value: null 20 | default: https://api.protonmail.ch 21 | prompt: enter the protonmail api baseurl 22 | 23 | -------------------------------------------------------------------------------- /curlz/src/curlz/domain/bookmark/load_bookmark.rs: -------------------------------------------------------------------------------- 1 | use crate::domain::bookmark::{Bookmark, BookmarkCollection}; 2 | use crate::domain::http::HttpMethod; 3 | 4 | #[derive(Debug)] 5 | pub struct LoadBookmark { 6 | pub slug: String, 7 | pub http_method: HttpMethod, 8 | } 9 | 10 | impl LoadBookmark { 11 | pub fn new(slug: impl AsRef, http_method: HttpMethod) -> Self { 12 | Self { 13 | slug: slug.as_ref().to_owned(), 14 | http_method, 15 | } 16 | } 17 | } 18 | 19 | pub fn load_bookmark(bm: LoadBookmark, collection: &impl BookmarkCollection) -> Option { 20 | collection.load(bm.slug, &bm.http_method).unwrap() 21 | } 22 | -------------------------------------------------------------------------------- /curlz/src/curlz/domain/bookmark/mod.rs: -------------------------------------------------------------------------------- 1 | mod collection; 2 | mod collection_impl; 3 | mod load_bookmark; 4 | mod save_bookmark; 5 | 6 | pub use self::collection::BookmarkCollection; 7 | pub use self::collection_impl::BookmarkFolderCollection; 8 | pub use self::load_bookmark::*; 9 | pub use self::save_bookmark::*; 10 | 11 | use crate::domain::http::HttpRequest; 12 | 13 | use serde::{Deserialize, Serialize}; 14 | 15 | #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] 16 | pub struct Bookmark { 17 | pub slug: String, 18 | pub request: HttpRequest, 19 | } 20 | 21 | impl AsRef for Bookmark { 22 | fn as_ref(&self) -> &HttpRequest { 23 | &self.request 24 | } 25 | } 26 | 27 | impl AsRef for Bookmark { 28 | fn as_ref(&self) -> &str { 29 | self.slug.as_ref() 30 | } 31 | } 32 | 33 | impl Bookmark { 34 | pub fn slug(&self) -> &str { 35 | self.as_ref() 36 | } 37 | 38 | pub fn request(&self) -> &HttpRequest { 39 | self.as_ref() 40 | } 41 | } 42 | 43 | impl<'a> From<&SaveBookmark<'a>> for Bookmark { 44 | fn from(cmd: &SaveBookmark<'a>) -> Self { 45 | Self { 46 | slug: cmd.slug.clone(), 47 | request: cmd.bookmark.clone(), 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /curlz/src/curlz/domain/bookmark/save_bookmark.rs: -------------------------------------------------------------------------------- 1 | use crate::domain::bookmark::collection::BookmarkCollection; 2 | use crate::domain::bookmark::Bookmark; 3 | use crate::domain::http::HttpRequest; 4 | 5 | #[derive(Debug)] 6 | pub struct SaveBookmark<'a> { 7 | pub slug: String, 8 | pub bookmark: &'a HttpRequest, 9 | } 10 | 11 | impl<'a> SaveBookmark<'a> { 12 | pub fn new(slug: impl AsRef, bookmark: &'a HttpRequest) -> Self { 13 | Self { 14 | slug: slug.as_ref().to_owned(), 15 | bookmark, 16 | } 17 | } 18 | } 19 | 20 | pub fn save_bookmark( 21 | bm: SaveBookmark, 22 | collection: &mut impl BookmarkCollection, 23 | ) -> crate::Result<()> { 24 | collection.save(&Bookmark::from(&bm)) 25 | } 26 | -------------------------------------------------------------------------------- /curlz/src/curlz/domain/environment/dot_env.rs: -------------------------------------------------------------------------------- 1 | use crate::domain::environment::Environment; 2 | use std::path::{Path, PathBuf}; 3 | 4 | /// wraps a `.env` file, used to create an [`Environment`] 5 | pub struct DotEnvFile(PathBuf); 6 | 7 | /// turn a [`PathBuf`] into a [`DotEnvFile`] 8 | impl From<&Path> for DotEnvFile { 9 | fn from(path: &Path) -> Self { 10 | Self(path.to_path_buf()) 11 | } 12 | } 13 | 14 | impl TryFrom for Environment { 15 | type Error = anyhow::Error; 16 | 17 | fn try_from(value: DotEnvFile) -> Result { 18 | let mut env = Environment::default(); 19 | dotenvy::from_path_iter(value.0.as_path()) 20 | .map_err(anyhow::Error::from)? 21 | .map(|i| i.unwrap()) 22 | .for_each(|(key, value)| { 23 | env.as_mut().insert(key, value); 24 | }); 25 | 26 | Ok(env) 27 | } 28 | } 29 | 30 | #[cfg(test)] 31 | mod tests { 32 | use super::*; 33 | use crate::test_utils::create_file; 34 | use indoc::indoc; 35 | 36 | #[test] 37 | fn should_try_from_dot_env_file() { 38 | let tmp = create_file( 39 | ".env", 40 | indoc! { r#" 41 | protonmail_api_baseurl=https://api.protonmail.ch 42 | email=some@user.com 43 | "#}, 44 | ) 45 | .unwrap(); 46 | let dot_file = DotEnvFile(tmp.path().join(".env")); 47 | 48 | let env = Environment::try_from(dot_file).unwrap(); 49 | assert_eq!( 50 | env.get("protonmail_api_baseurl").unwrap().as_ref(), 51 | "https://api.protonmail.ch" 52 | ); 53 | assert_eq!(env.get("email").unwrap().as_ref(), "some@user.com"); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /curlz/src/curlz/domain/environment/env.rs: -------------------------------------------------------------------------------- 1 | use crate::template::variables::Placeholder; 2 | use std::collections::HashMap; 3 | use std::ffi::OsStr; 4 | use std::path::Path; 5 | 6 | use super::dot_env::DotEnvFile; 7 | use super::yaml_env::YamlEnvFile; 8 | 9 | #[derive(Default, Debug)] 10 | pub struct Environment(pub(crate) HashMap); 11 | 12 | impl Environment { 13 | /// returns the value for a given key 14 | pub fn get(&'_ self, key: impl AsRef) -> Option + '_> { 15 | self.0.get(key.as_ref()) 16 | } 17 | 18 | /// inserts a key with it's value, copies the data 19 | pub fn insert(&mut self, key: impl AsRef, value: impl AsRef) { 20 | self.0 21 | .insert(key.as_ref().to_string(), value.as_ref().to_string()); 22 | } 23 | } 24 | 25 | impl AsMut> for Environment { 26 | fn as_mut(&mut self) -> &mut HashMap { 27 | &mut self.0 28 | } 29 | } 30 | 31 | impl TryFrom<&Path> for Environment { 32 | type Error = anyhow::Error; 33 | 34 | fn try_from(env_file: &Path) -> Result { 35 | if env_file.exists() && env_file.is_file() { 36 | match env_file.extension().and_then(OsStr::to_str) { 37 | None => DotEnvFile::from(env_file).try_into(), 38 | Some("yml" | "yaml") => YamlEnvFile::from(env_file).try_into(), 39 | Some(ext) => todo!("Environment loading for file extension {}", ext), 40 | } 41 | } else if env_file.is_dir() { 42 | todo!("Support directory environments"); 43 | } else { 44 | Ok(Environment::default()) 45 | } 46 | } 47 | } 48 | 49 | impl From<&Environment> for minijinja::value::Value { 50 | fn from(env: &Environment) -> Self { 51 | minijinja::value::Value::from_serializable(&env.0) 52 | } 53 | } 54 | 55 | /// creates an [`Environment`] from a `.env` | `.yaml` | `.yml` file 56 | /// If the file does not exist, an empty [`Environment`] is returned. 57 | /// 58 | /// ## Fallible 59 | /// If `env_file` is not a `.env` | `.yaml` | `.yml` file, an error is returned. 60 | /// If `env_file` is a directory, an error is returned. 61 | pub fn create_environment( 62 | env_file: impl AsRef, 63 | placeholders: &[Placeholder], 64 | ) -> crate::Result { 65 | Environment::try_from(env_file.as_ref()).map(|mut env| { 66 | placeholders 67 | .iter() 68 | .map(|placeholder| { 69 | let Placeholder { 70 | name, 71 | value, 72 | default, 73 | .. 74 | } = placeholder; 75 | (name, value.as_ref().or(default.as_ref()).unwrap()) 76 | }) 77 | .for_each(|(k, v)| env.insert(k, v)); 78 | env 79 | }) 80 | } 81 | 82 | #[cfg(test)] 83 | mod tests { 84 | use super::*; 85 | use crate::test_utils::create_file; 86 | 87 | #[test] 88 | fn should_load_a_dot_env_file() { 89 | let tmp = create_file(".env", "").unwrap(); 90 | Environment::try_from(tmp.path().join(".env").as_path()).unwrap(); 91 | } 92 | 93 | #[test] 94 | fn should_load_a_yaml_env_file() { 95 | let tmp = create_file("staging.yml", "").unwrap(); 96 | Environment::try_from(tmp.path().join(".staging.yml").as_path()).unwrap(); 97 | } 98 | 99 | #[test] 100 | fn should_gracefully_ignore_not_existing_files() { 101 | Environment::try_from(Path::new("foo.bar.yml")).unwrap(); 102 | } 103 | 104 | #[test] 105 | #[should_panic(expected = "not yet implemented: Environment loading for file extension json")] 106 | fn should_not_load_a_json_env_file() { 107 | let tmp = create_file("staging.json", "").unwrap(); 108 | Environment::try_from(tmp.path().join("staging.json").as_path()).unwrap(); 109 | } 110 | 111 | #[test] 112 | #[should_panic(expected = "not yet implemented: Support directory environments")] 113 | fn should_not_load_a_env_directory() { 114 | let tmp = create_file("env/staging.yml", "").unwrap(); 115 | Environment::try_from(tmp.path().join("env").as_path()).unwrap(); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /curlz/src/curlz/domain/environment/mod.rs: -------------------------------------------------------------------------------- 1 | mod dot_env; 2 | mod env; 3 | mod yaml_env; 4 | 5 | pub use dot_env::*; 6 | pub use env::*; 7 | pub use yaml_env::*; 8 | -------------------------------------------------------------------------------- /curlz/src/curlz/domain/environment/yaml_env.rs: -------------------------------------------------------------------------------- 1 | use super::env::Environment; 2 | 3 | use std::fs::File; 4 | use std::path::{Path, PathBuf}; 5 | 6 | /// wraps a `.yml` file, used to create an [`Environment`] 7 | pub struct YamlEnvFile(PathBuf); 8 | 9 | /// turn a [`PathBuf`] into a [`YamlEnvFile`] 10 | impl From<&Path> for YamlEnvFile { 11 | fn from(path: &Path) -> Self { 12 | Self(path.to_path_buf()) 13 | } 14 | } 15 | 16 | impl TryFrom for Environment { 17 | type Error = anyhow::Error; 18 | 19 | fn try_from(value: YamlEnvFile) -> Result { 20 | let file = File::open(value.0.as_path())?; 21 | serde_yaml::from_reader(file) 22 | .map(Self) 23 | .map_err(|e| e.into()) 24 | } 25 | } 26 | 27 | #[cfg(test)] 28 | mod tests { 29 | use super::*; 30 | use crate::test_utils::create_file; 31 | use indoc::indoc; 32 | 33 | #[test] 34 | fn should_try_from_yaml_env_file() { 35 | let tmp = create_file( 36 | ".staging.yml", 37 | indoc! { r#" 38 | protonmail_api_baseurl: https://api.protonmail.ch 39 | email: some@user.com 40 | "#}, 41 | ) 42 | .unwrap(); 43 | let dot_file = YamlEnvFile(tmp.path().join(".staging.yml")); 44 | 45 | let env = Environment::try_from(dot_file).unwrap(); 46 | assert_eq!( 47 | env.get("protonmail_api_baseurl").unwrap().as_ref(), 48 | "https://api.protonmail.ch" 49 | ); 50 | assert_eq!(env.get("email").unwrap().as_ref(), "some@user.com"); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /curlz/src/curlz/domain/http/http_body.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 6 | pub enum HttpBody { 7 | InlineText(String), 8 | InlineBinary(Vec), 9 | Extern(PathBuf), 10 | None, 11 | } 12 | 13 | impl Default for HttpBody { 14 | fn default() -> Self { 15 | Self::None 16 | } 17 | } 18 | 19 | impl HttpBody { 20 | // todo: not in sync with `.as_bytes()` 21 | pub fn contents(&self) -> std::io::Result> { 22 | Ok(match self { 23 | HttpBody::InlineText(c) => Some(c.to_owned()), 24 | HttpBody::InlineBinary(_) => todo!("Binary data cannot be represented as string yet"), 25 | HttpBody::Extern(f) => Some(std::fs::read_to_string(f)?), 26 | HttpBody::None => None, 27 | }) 28 | } 29 | 30 | // todo: not in sync with `.contents()` 31 | pub fn as_bytes(&self) -> std::io::Result<&[u8]> { 32 | Ok(match self { 33 | HttpBody::InlineText(t) => t.as_bytes(), 34 | HttpBody::InlineBinary(b) => b.as_slice(), 35 | HttpBody::Extern(_) => todo!("not yet there.."), 36 | // HttpBody::Extern(e) => { 37 | // let mut f = fs::File::open(e).unwrap(); 38 | // let mut data = Vec::new(); 39 | // f.read_to_end(&mut data).unwrap(); 40 | // data.as_slice() 41 | // } 42 | HttpBody::None => b"", 43 | }) 44 | } 45 | } 46 | 47 | impl From<&str> for HttpBody { 48 | fn from(value: &str) -> Self { 49 | Self::InlineText(value.to_string()) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /curlz/src/curlz/domain/http/http_headers.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Default, Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 4 | pub struct HttpHeaders(Vec<(String, String)>); 5 | 6 | impl HttpHeaders { 7 | /// reverses the order of headers 8 | pub fn reverse(&mut self) { 9 | self.0.reverse(); 10 | } 11 | } 12 | 13 | impl HttpHeaders { 14 | /// adds a new header in the form of key, value 15 | pub fn push(&mut self, key: impl AsRef, value: impl AsRef) { 16 | self.0 17 | .push((key.as_ref().to_string(), value.as_ref().to_string())); 18 | } 19 | 20 | pub fn merge(&mut self, other: &HttpHeaders) { 21 | self.0.extend(other.0.iter().cloned()); 22 | } 23 | 24 | pub fn get(&self, key: impl Into) -> Option<&str> { 25 | let key = key.into(); 26 | self.0 27 | .iter() 28 | .find(|(hn, _)| hn.as_str() == key) 29 | .map(|x| x.1.as_ref()) 30 | } 31 | } 32 | 33 | impl AsRef<[(String, String)]> for HttpHeaders { 34 | fn as_ref(&self) -> &[(String, String)] { 35 | self.0.as_slice() 36 | } 37 | } 38 | 39 | impl From<&[String]> for HttpHeaders { 40 | fn from(headers: &[String]) -> Self { 41 | Self( 42 | headers 43 | .iter() 44 | .map(|value| { 45 | let (key, value) = value.split_once(':').unwrap(); 46 | (key.trim().to_string(), value.trim().to_string()) 47 | }) 48 | .collect(), 49 | ) 50 | } 51 | } 52 | 53 | #[cfg(test)] 54 | mod tests { 55 | use super::*; 56 | use crate::cli::HeaderArgs; 57 | 58 | #[test] 59 | fn should_parse_the_header_arg_flag_away() { 60 | let args = vec![ 61 | "-H", 62 | "foo: bar", 63 | "--header", 64 | "Accept: application/json", 65 | "--header", 66 | r#"Authorization: Baerer {{ jwt({"foo": "bar"}) }}"#, 67 | "http://example.com", 68 | ] 69 | .iter() 70 | .map(ToString::to_string) 71 | .collect::>(); 72 | 73 | let headers: HeaderArgs = (&args).into(); 74 | // let headers: HttpHeaders = (&*args).into(); 75 | let headers: HttpHeaders = headers.into(); 76 | 77 | assert_eq!( 78 | headers.as_ref(), 79 | &[ 80 | ("foo".to_string(), "bar".to_string()), 81 | ("Accept".to_string(), "application/json".to_string()), 82 | ( 83 | "Authorization".to_string(), 84 | r#"Baerer {{ jwt({"foo": "bar"}) }}"#.to_string() 85 | ) 86 | ] 87 | ); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /curlz/src/curlz/domain/http/http_method.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow; 2 | use serde::{Deserialize, Serialize}; 3 | use std::str::FromStr; 4 | 5 | #[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] 6 | #[serde(rename_all = "UPPERCASE")] 7 | pub enum HttpMethod { 8 | Get, 9 | Post, 10 | Put, 11 | Delete, 12 | Head, 13 | Options, 14 | Trace, 15 | Connect, 16 | Patch, 17 | } 18 | 19 | impl From<&HttpMethod> for String { 20 | fn from(method: &HttpMethod) -> Self { 21 | format!("{:?}", method).to_uppercase() 22 | } 23 | } 24 | 25 | impl FromStr for HttpMethod { 26 | type Err = anyhow::Error; 27 | 28 | fn from_str(s: &str) -> Result { 29 | serde_yaml::from_str(s.to_uppercase().as_str()) 30 | .map_err(|_| anyhow!("Unsupported HTTP method: {}", s)) 31 | } 32 | } 33 | 34 | #[cfg(test)] 35 | mod tests { 36 | use super::*; 37 | 38 | #[test] 39 | fn should_parse_str_to_http_method_gracefully() { 40 | assert_eq!("Get".parse::().unwrap(), HttpMethod::Get); 41 | assert_eq!("GET".parse::().unwrap(), HttpMethod::Get); 42 | assert_eq!("get".parse::().unwrap(), HttpMethod::Get); 43 | } 44 | 45 | #[test] 46 | #[should_panic(expected = "Unsupported HTTP method: Pal")] 47 | fn should_throw_unsupported_methods() { 48 | "Pal".parse::().unwrap(); 49 | } 50 | 51 | #[test] 52 | fn should_convert_to_uppercase_string() { 53 | let method: String = (&HttpMethod::Get).into(); 54 | assert_eq!(method, "GET"); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /curlz/src/curlz/domain/http/http_request.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use crate::domain::http::{HttpBody, HttpHeaders, HttpMethod, HttpUri, HttpVersion}; 4 | use crate::template::variables::Placeholder; 5 | 6 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 7 | pub struct HttpRequest { 8 | pub url: HttpUri, 9 | pub method: HttpMethod, 10 | pub version: HttpVersion, 11 | pub headers: HttpHeaders, 12 | pub body: HttpBody, 13 | pub curl_params: Vec, 14 | pub placeholders: Vec, 15 | } 16 | 17 | impl HttpRequest { 18 | pub fn update(&self, update_fn: impl Fn(&mut Self)) -> Self { 19 | let mut req = self.clone(); 20 | update_fn(&mut req); 21 | 22 | req 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /curlz/src/curlz/domain/http/http_uri.rs: -------------------------------------------------------------------------------- 1 | //! # HTTP URI 2 | //! Describes a HTTP URI with template variables or functions contained 3 | //! 4 | //! ## not yet implemented: 5 | //! - [ ] TODO: impl validation for URIs with placeholders 6 | 7 | use serde::{Deserialize, Serialize}; 8 | 9 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 10 | pub struct HttpUri(String); 11 | 12 | impl AsRef for HttpUri { 13 | fn as_ref(&self) -> &str { 14 | self.0.as_ref() 15 | } 16 | } 17 | 18 | impl From<&str> for HttpUri { 19 | fn from(value: &str) -> Self { 20 | Self(value.to_owned()) 21 | } 22 | } 23 | 24 | impl TryFrom for HttpUri { 25 | type Error = anyhow::Error; 26 | 27 | fn try_from(value: String) -> Result { 28 | // todo: validation might go here 29 | Ok(Self(value)) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /curlz/src/curlz/domain/http/http_version.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow; 2 | use serde::{Deserialize, Serialize}; 3 | use std::str::FromStr; 4 | 5 | #[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] 6 | #[serde(rename_all = "UPPERCASE")] 7 | pub enum HttpVersion { 8 | #[serde(rename = "HTTP/1.1")] 9 | Http11, 10 | #[serde(rename = "HTTP/2")] 11 | Http2, 12 | #[serde(rename = "HTTP/3")] 13 | Http3, 14 | } 15 | 16 | impl From<&HttpVersion> for String { 17 | fn from(version: &HttpVersion) -> Self { 18 | serde_yaml::to_string(version) 19 | .unwrap() 20 | .trim_end() 21 | .to_string() 22 | } 23 | } 24 | 25 | impl FromStr for HttpVersion { 26 | type Err = anyhow::Error; 27 | 28 | fn from_str(s: &str) -> Result { 29 | serde_yaml::from_str(s.to_uppercase().as_str()) 30 | .map_err(|_| anyhow!("Unsupported HTTP version: {}", s)) 31 | } 32 | } 33 | 34 | #[cfg(test)] 35 | mod tests { 36 | use super::*; 37 | 38 | #[test] 39 | fn should_parse_str_to_http_method_gracefully() { 40 | assert_eq!( 41 | "HTTP/1.1".parse::().unwrap(), 42 | HttpVersion::Http11 43 | ); 44 | assert_eq!( 45 | "http/1.1".parse::().unwrap(), 46 | HttpVersion::Http11 47 | ); 48 | } 49 | 50 | #[test] 51 | #[should_panic(expected = "Unsupported HTTP version: HTTP/1.0")] 52 | fn should_throw_unsupported_methods() { 53 | "HTTP/1.0".parse::().unwrap(); 54 | } 55 | 56 | #[test] 57 | fn should_convert_to_uppercase_string() { 58 | let version: String = (&HttpVersion::Http11).into(); 59 | assert_eq!(version, "HTTP/1.1"); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /curlz/src/curlz/domain/http/mod.rs: -------------------------------------------------------------------------------- 1 | mod http_body; 2 | mod http_headers; 3 | mod http_method; 4 | mod http_request; 5 | mod http_uri; 6 | mod http_version; 7 | 8 | pub use http_body::*; 9 | pub use http_headers::*; 10 | pub use http_method::*; 11 | pub use http_request::*; 12 | pub use http_uri::*; 13 | pub use http_version::*; 14 | -------------------------------------------------------------------------------- /curlz/src/curlz/domain/http_lang/http-lang-grammar.pest: -------------------------------------------------------------------------------- 1 | request = { request_line ~ headers? ~ (NEWLINE ~ body)? } 2 | 3 | request_line = _{ method ~ SPACE_SEPARATOR ~ uri ~ SPACE_SEPARATOR+ ~ version ~ SPACE_SEPARATOR* ~ NEWLINE } 4 | uri = { (!SPACE_SEPARATOR ~ ANY)+ } 5 | method = { "GET" | "POST" | "PUT" | "DELETE" } 6 | version = { "HTTP/" ~ (ASCII_DIGIT | ".")+ } 7 | 8 | headers = { header+ } 9 | header = { header_name ~ SPACE_SEPARATOR* ~ ":" ~ SPACE_SEPARATOR* ~ header_value ~ SPACE_SEPARATOR* ~ NEWLINE } 10 | header_name = { (!(":" | NEWLINE) ~ ANY)+ } 11 | header_value = { (!NEWLINE ~ ANY)+ } 12 | 13 | body = { !NEWLINE ~ (!delimiter ~ ANY)* } 14 | delimiter = { "#"{3} ~ (!NEWLINE ~ ANY)+ ~ SPACE_SEPARATOR* ~ NEWLINE+ } 15 | 16 | file = { SOI ~ (delimiter? ~ request) ~ (delimiter ~ request)* ~ EOI} 17 | -------------------------------------------------------------------------------- /curlz/src/curlz/domain/http_lang/mod.rs: -------------------------------------------------------------------------------- 1 | mod parse_request; 2 | 3 | pub use parse_request::*; 4 | -------------------------------------------------------------------------------- /curlz/src/curlz/domain/http_lang/parse_request.rs: -------------------------------------------------------------------------------- 1 | //! a module for experimenting with the http language that the rest client uses 2 | use crate::domain::bookmark::Bookmark; 3 | use crate::domain::http::{HttpBody, HttpHeaders, HttpMethod, HttpRequest, HttpUri, HttpVersion}; 4 | 5 | use anyhow::anyhow; 6 | use pest::iterators::Pair; 7 | use pest::Parser; 8 | 9 | #[derive(Parser)] 10 | #[grammar = "curlz/domain/http_lang/http-lang-grammar.pest"] // relative to project `src` 11 | struct HttpParser; 12 | 13 | #[inline] 14 | fn trimmed_string(rule: Pair<'_, T>) -> String { 15 | rule.as_str() 16 | .trim() 17 | .chars() 18 | .filter(|x| !['\n', '\r'].contains(x)) 19 | .collect() 20 | } 21 | 22 | pub fn parse_request_file(req_file: impl AsRef) -> Result, anyhow::Error> { 23 | let mut requests = vec![]; 24 | 25 | let req_file = req_file.as_ref(); 26 | let file = HttpParser::parse(Rule::file, req_file)?.next().unwrap(); 27 | 28 | let mut delimiter: String = "".to_owned(); 29 | for line in file.into_inner() { 30 | match line.as_rule() { 31 | Rule::request => { 32 | requests.push(Bookmark { 33 | slug: delimiter.to_owned(), 34 | request: HttpRequest::try_from(line)?, 35 | }); 36 | } 37 | Rule::delimiter => delimiter = trimmed_string(line), 38 | Rule::EOI => {} 39 | x => { 40 | todo!("x = {:?}\n", x); 41 | } 42 | } 43 | } 44 | 45 | Ok(requests) 46 | } 47 | 48 | /// todo: write tests 49 | impl TryFrom> for HttpHeaders { 50 | type Error = anyhow::Error; 51 | 52 | fn try_from(headers: Pair<'_, Rule>) -> Result { 53 | match headers.as_rule() { 54 | Rule::headers => { 55 | let mut h: HttpHeaders = Default::default(); 56 | for header in headers.into_inner() { 57 | let mut inner_rules = header.into_inner(); 58 | 59 | let name = trimmed_string(inner_rules.next().unwrap()); 60 | let value = trimmed_string(inner_rules.next().unwrap()); 61 | 62 | h.push(name, value); 63 | } 64 | Ok(h) 65 | } 66 | _ => Err(anyhow!("The parsing result are not a valid `headers`")), 67 | } 68 | } 69 | } 70 | 71 | /// todo: write tests 72 | impl TryFrom> for HttpRequest { 73 | type Error = anyhow::Error; 74 | fn try_from(request: Pair<'_, Rule>) -> Result { 75 | match request.as_rule() { 76 | Rule::request => { 77 | let mut inner_rules = request.into_inner(); 78 | let method: HttpMethod = inner_rules.next().unwrap().try_into()?; 79 | let url: HttpUri = inner_rules.next().unwrap().try_into()?; 80 | let version: HttpVersion = inner_rules.next().unwrap().try_into()?; 81 | let headers = inner_rules 82 | .next() 83 | .map(HttpHeaders::try_from) 84 | .unwrap_or_else(|| Ok(HttpHeaders::default()))?; 85 | let body = inner_rules 86 | .next() 87 | .map(HttpBody::try_from) 88 | // todo: maybe an error on parsing remains an error 89 | .map(|b| b.unwrap_or_default()) 90 | .unwrap_or_default(); 91 | 92 | Ok(Self { 93 | url, 94 | method, 95 | version, 96 | headers, 97 | body, 98 | curl_params: Default::default(), 99 | placeholders: Default::default(), 100 | }) 101 | } 102 | _ => Err(anyhow!("The parsing result is not a valid `request`")), 103 | } 104 | } 105 | } 106 | 107 | impl TryFrom> for HttpUri { 108 | type Error = anyhow::Error; 109 | 110 | fn try_from(value: Pair<'_, Rule>) -> Result { 111 | match value.as_rule() { 112 | Rule::uri => value.as_str().to_string().try_into(), 113 | _ => Err(anyhow!("The parsing result is not a valid `uri`")), 114 | } 115 | } 116 | } 117 | 118 | impl TryFrom> for HttpMethod { 119 | type Error = anyhow::Error; 120 | 121 | fn try_from(value: Pair<'_, Rule>) -> Result { 122 | match value.as_rule() { 123 | Rule::method => value.as_str().parse::(), 124 | _ => Err(anyhow!("The parsing result is not a valid `method`")), 125 | } 126 | } 127 | } 128 | 129 | /// converts `Pairs` into [`HttpVersion`] 130 | impl TryFrom> for HttpVersion { 131 | type Error = anyhow::Error; 132 | fn try_from(value: Pair<'_, Rule>) -> Result { 133 | match value.as_rule() { 134 | Rule::version => value.as_str().parse::(), 135 | _ => Err(anyhow!("The parsing result is not a valid `version`")), 136 | } 137 | } 138 | } 139 | 140 | /// converts the body 141 | impl TryFrom> for HttpBody { 142 | type Error = anyhow::Error; 143 | fn try_from(value: Pair<'_, Rule>) -> Result { 144 | match value.as_rule() { 145 | Rule::body => Ok(HttpBody::InlineText(value.as_str().to_owned())), 146 | _ => Err(anyhow!("The parsing result is not a valid `version`")), 147 | } 148 | } 149 | } 150 | 151 | #[cfg(test)] 152 | mod tests { 153 | use super::*; 154 | use crate::domain::http::HttpVersion::Http11; 155 | use crate::domain::http::*; 156 | 157 | use indoc::indoc; 158 | use pretty_assertions::assert_eq; 159 | use rstest::rstest; 160 | 161 | #[rstest] 162 | #[case( 163 | indoc! {r#" 164 | ### GET gitignore template for rustlang 165 | GET https://api.github.com/gitignore/templates/Rust HTTP/1.1 166 | Accept: application/json 167 | "#}, 168 | Bookmark { 169 | slug: "### GET gitignore template for rustlang".into(), 170 | request: HttpRequest { 171 | url: "https://api.github.com/gitignore/templates/Rust".into(), 172 | method: HttpMethod::Get, 173 | version: HttpVersion::Http11, 174 | headers: HttpHeaders::from(["Accept: application/json".to_owned()].as_slice()), 175 | body: HttpBody::default(), 176 | curl_params: Default::default(), 177 | placeholders: Default::default(), 178 | } 179 | } 180 | )] 181 | #[case( 182 | indoc! {r#" 183 | ### GET request with environment variables 184 | GET https://api.github.com/gitignore/templates/Rust HTTP/1.1 185 | "#}, 186 | Bookmark { 187 | slug: "### GET request with environment variables".into(), 188 | request: HttpRequest { 189 | url: "https://api.github.com/gitignore/templates/Rust".into(), 190 | method: HttpMethod::Get, 191 | version: HttpVersion::Http11, 192 | headers: Default::default(), 193 | body: HttpBody::default(), 194 | curl_params: Default::default(), 195 | placeholders: Default::default(), 196 | } 197 | } 198 | )] 199 | #[case( 200 | indoc! {r#" 201 | ### this is a POST request with a body 202 | POST https://httpbin.org/anything HTTP/1.1 203 | Accept: application/json 204 | Content-Type: application/json 205 | 206 | { 207 | "foo": "Bar", 208 | "bool": true 209 | } 210 | "#}, 211 | Bookmark { 212 | slug: "### this is a POST request with a body".into(), 213 | request: HttpRequest { 214 | url: "https://httpbin.org/anything".into(), 215 | method: HttpMethod::Post, 216 | version: HttpVersion::Http11, 217 | headers: HttpHeaders::from([ 218 | "Accept: application/json".to_owned(), 219 | "Content-Type: application/json".to_owned(), 220 | ].as_slice()), 221 | body: HttpBody::InlineText(indoc! {r#" 222 | { 223 | "foo": "Bar", 224 | "bool": true 225 | } 226 | "#}.to_owned()), 227 | curl_params: Default::default(), 228 | placeholders: Default::default(), 229 | } 230 | } 231 | )] 232 | #[case( 233 | indoc! {r#" 234 | ### this is a POST request with a body 235 | POST https://httpbin.org/anything HTTP/1.1 236 | Accept : application/json 237 | Content-Type :application/json 238 | 239 | { 240 | "foo": "Bar", 241 | "bool": true 242 | } 243 | "#}, 244 | Bookmark { 245 | slug: "### this is a POST request with a body".into(), 246 | request: HttpRequest { 247 | url: "https://httpbin.org/anything".into(), 248 | method: HttpMethod::Post, 249 | version: HttpVersion::Http11, 250 | headers: HttpHeaders::from([ 251 | "Accept: application/json".to_owned(), 252 | "Content-Type: application/json".to_owned(), 253 | ].as_slice()), 254 | body: HttpBody::InlineText(indoc! {r#" 255 | { 256 | "foo": "Bar", 257 | "bool": true 258 | } 259 | "#}.to_owned()), 260 | curl_params: Default::default(), 261 | placeholders: Default::default(), 262 | } 263 | } 264 | )] 265 | fn should_parse_a_http_message( 266 | #[case] request_file_contents: &str, 267 | #[case] expected: Bookmark, 268 | ) { 269 | assert_eq!( 270 | parse_request_file(request_file_contents) 271 | .unwrap() 272 | .pop() 273 | .unwrap(), 274 | expected 275 | ); 276 | } 277 | 278 | mod http_version { 279 | use super::*; 280 | use pretty_assertions::assert_eq; 281 | 282 | #[test] 283 | fn test_http_version_parsing() { 284 | let version = HttpParser::parse(Rule::version, "HTTP/1.1") 285 | .expect("parsing failed!") 286 | .next() 287 | .unwrap(); 288 | let version: HttpVersion = version.try_into().unwrap(); 289 | assert_eq!(version, Http11); 290 | } 291 | 292 | #[test] 293 | #[should_panic(expected = "Unsupported HTTP version: HTTP/1.0")] 294 | fn test_http_version_parsing_unsupported_version() { 295 | let _: HttpVersion = HttpParser::parse(Rule::version, "HTTP/1.0") 296 | .unwrap() 297 | .next() 298 | .unwrap() 299 | .try_into() 300 | .unwrap(); 301 | } 302 | 303 | #[test] 304 | #[should_panic] 305 | fn test_http_version_parsing_parsing_error() { 306 | HttpParser::parse(Rule::version, "http/") 307 | .unwrap() 308 | .next() 309 | .unwrap(); 310 | } 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /curlz/src/curlz/domain/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod bookmark; 2 | pub mod environment; 3 | pub mod http; 4 | pub mod request; 5 | 6 | #[cfg(feature = "x-http-lang")] 7 | pub mod http_lang; 8 | -------------------------------------------------------------------------------- /curlz/src/curlz/domain/request/backend/invoke_curl.rs: -------------------------------------------------------------------------------- 1 | use self::curl_arg_conversions::IntoCurlArguments; 2 | use super::HttpBackend; 3 | use crate::domain::http::HttpBody; 4 | use crate::domain::request::{IssueRequest, RequestContext, Verbosity}; 5 | use crate::Result; 6 | 7 | use anyhow::Context; 8 | use log::debug; 9 | use std::process::{Command, Stdio}; 10 | 11 | #[derive(Default)] 12 | pub struct InvokeCurlBackend; 13 | 14 | /// It knows haw to issue a `HttpRequest` 15 | impl HttpBackend for InvokeCurlBackend { 16 | fn issue(&self, req: &IssueRequest, context: &RequestContext) -> Result<()> { 17 | let request = req.request; 18 | let mut renderer = context.renderer_with_placeholders(&request.placeholders); 19 | 20 | let url = renderer.render(request.url.as_ref(), "url")?; 21 | let _method: String = (&request.method).into(); 22 | 23 | let mut cmd = Command::new("curl"); 24 | if req.verbosity.eq(&Verbosity::Silent) { 25 | cmd.arg("-s"); 26 | } 27 | let payload = if request.body.ne(&HttpBody::None) { 28 | vec![ 29 | "--data".to_string(), 30 | match &request.body { 31 | HttpBody::InlineText(s) => renderer.render(s.as_str(), "body")?, 32 | HttpBody::InlineBinary(_) => todo!("inline binary data not impl yet"), 33 | HttpBody::Extern(_) => todo!("external file data loading impl yet"), 34 | HttpBody::None => "".to_string(), 35 | }, 36 | ] 37 | } else { 38 | vec![] 39 | }; 40 | 41 | cmd.args(request.method.as_curl_parameter()) 42 | .args( 43 | &request 44 | .curl_params 45 | .iter() 46 | .map(|s| renderer.render(s, "param")) 47 | .collect::>>()?, 48 | ) 49 | .args(request.headers.as_ref().iter().flat_map(|(k, v)| { 50 | let value = renderer.render(v, k).unwrap(); 51 | vec!["-H".to_string(), format!("{}: {}", k, value)] 52 | })) 53 | .arg(&url) 54 | .args(&payload); 55 | 56 | debug!("curl cmd: \n {:?}", &cmd); 57 | 58 | cmd.stdout(Stdio::inherit()) 59 | .stderr(Stdio::inherit()) 60 | .output() 61 | .map(|_output| ()) 62 | .context("error when starting curl") 63 | } 64 | } 65 | 66 | mod curl_arg_conversions { 67 | use crate::domain::http::HttpMethod; 68 | use std::ffi::OsStr; 69 | 70 | /// consumes self, and turns it into arguments for a curl [`Command`] 71 | /// 72 | pub trait IntoCurlArguments { 73 | fn as_curl_parameter(&self) -> I 74 | where 75 | I: IntoIterator, 76 | S: AsRef; 77 | } 78 | 79 | /// todo: not yet sure if that abstraction is really helpful or stands in the way 80 | impl IntoCurlArguments, String> for HttpMethod { 81 | fn as_curl_parameter(&self) -> Vec { 82 | let method: String = self.into(); 83 | vec!["-X".to_string(), method] 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /curlz/src/curlz/domain/request/backend/mod.rs: -------------------------------------------------------------------------------- 1 | mod invoke_curl; 2 | 3 | use super::IssueRequest; 4 | use crate::domain::request::RequestContext; 5 | pub use invoke_curl::*; 6 | 7 | pub trait HttpBackend { 8 | fn issue(&self, request: &IssueRequest, context: &RequestContext) -> crate::Result<()>; 9 | } 10 | -------------------------------------------------------------------------------- /curlz/src/curlz/domain/request/context.rs: -------------------------------------------------------------------------------- 1 | use crate::domain::environment::Environment; 2 | use crate::template::variables::Placeholder; 3 | use crate::template::Renderer; 4 | 5 | /// processes all commands and keeps the application state 6 | pub struct RequestContext<'a> { 7 | environment: &'a Environment, 8 | } 9 | 10 | impl<'a> RequestContext<'a> { 11 | pub fn new(environment: &'a Environment) -> Self { 12 | Self { environment } 13 | } 14 | 15 | pub fn environment(&self) -> &Environment { 16 | self.environment 17 | } 18 | 19 | /// creates a new renderer based on the inner ['Environment`] 20 | pub fn renderer(&self) -> Renderer { 21 | self.environment.into() 22 | } 23 | 24 | /// creates a new renderer based on the inner [`Environment`] 25 | /// and the provided `placeholders` 26 | pub fn renderer_with_placeholders<'source>( 27 | &'source self, 28 | placeholders: &'source [Placeholder], 29 | ) -> Renderer<'source> { 30 | let mut r = self.renderer(); 31 | 32 | placeholders.iter().for_each(|placeholder| { 33 | let value = placeholder 34 | .value 35 | .as_ref() 36 | .unwrap_or_else(|| placeholder.default.as_ref().unwrap()); 37 | 38 | r.inject_variable(&placeholder.name, value.to_string()); 39 | }); 40 | 41 | r 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /curlz/src/curlz/domain/request/issue_request.rs: -------------------------------------------------------------------------------- 1 | use super::backend::*; 2 | use crate::domain::environment::Environment; 3 | use crate::domain::http::HttpRequest; 4 | use crate::domain::request::RequestContext; 5 | 6 | #[derive(Ord, PartialOrd, Eq, PartialEq, Clone)] 7 | pub enum Verbosity { 8 | Silent, 9 | Verbose, 10 | } 11 | 12 | pub struct IssueRequest<'r> { 13 | pub request: &'r HttpRequest, 14 | pub verbosity: Verbosity, 15 | } 16 | 17 | impl<'r> IssueRequest<'r> { 18 | pub fn new(request: &'r HttpRequest, verbosity: Verbosity) -> Self { 19 | Self { request, verbosity } 20 | } 21 | } 22 | 23 | /// issues a request with a given `backend` 24 | pub fn issue_request( 25 | req: IssueRequest<'_>, 26 | backend: &impl HttpBackend, 27 | env: &Environment, 28 | ) -> crate::Result<()> { 29 | let ctx = RequestContext::new(env); 30 | 31 | backend.issue(&req, &ctx) 32 | } 33 | 34 | /// issues a request with the via curl 35 | pub fn issue_request_with_curl(req: IssueRequest<'_>, env: &Environment) -> crate::Result<()> { 36 | let backend = InvokeCurlBackend::default(); 37 | 38 | issue_request(req, &backend, env) 39 | } 40 | 41 | #[cfg(test)] 42 | mod tests { 43 | use super::*; 44 | use crate::test_utils::sample_requests; 45 | use log::debug; 46 | 47 | #[derive(Default)] 48 | struct MockBackend; 49 | impl HttpBackend for MockBackend { 50 | fn issue(&self, _request: &IssueRequest, _context: &RequestContext) -> crate::Result<()> { 51 | debug!("MockBackend issues a request"); 52 | Ok(()) 53 | } 54 | } 55 | 56 | #[test] 57 | fn how_to_issue_a_request() { 58 | let env = Environment::default(); 59 | let req = sample_requests::post_request(); 60 | let req = IssueRequest::new(&req, Verbosity::Verbose); 61 | let backend = MockBackend::default(); 62 | let res = issue_request(req, &backend, &env); 63 | 64 | assert!(res.is_ok()) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /curlz/src/curlz/domain/request/mod.rs: -------------------------------------------------------------------------------- 1 | mod backend; 2 | mod context; 3 | mod issue_request; 4 | 5 | pub use context::RequestContext; 6 | pub use issue_request::*; 7 | -------------------------------------------------------------------------------- /curlz/src/curlz/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod cli; 2 | pub mod template; 3 | pub mod utils; 4 | 5 | // the main domain logic 6 | pub mod domain; 7 | 8 | #[cfg(feature = "x-http-lang")] 9 | #[macro_use] 10 | extern crate pest_derive; 11 | 12 | #[cfg(test)] 13 | pub mod test_utils; 14 | 15 | extern crate core; 16 | 17 | pub type Result = anyhow::Result; 18 | 19 | pub mod prelude {} 20 | -------------------------------------------------------------------------------- /curlz/src/curlz/template/functions/auth/basic.rs: -------------------------------------------------------------------------------- 1 | use base64::{alphabet, engine, Engine}; 2 | use minijinja::{Error, State}; 3 | 4 | /// generates a basic auth token, without the usual literal `Basic` 5 | pub(super) fn basic(_state: &State, user: &str, password: &str) -> Result { 6 | let engine = engine::GeneralPurpose::new(&alphabet::URL_SAFE, engine::general_purpose::PAD); 7 | let token = engine.encode(format!("{user}:{password}")); 8 | 9 | Ok(token) 10 | } 11 | 12 | #[cfg(test)] 13 | mod tests { 14 | use super::*; 15 | use crate::test_utils::RenderBuilder; 16 | 17 | #[test] 18 | fn should_encode_base64() { 19 | let e = RenderBuilder::new().with_function("basic", basic); 20 | 21 | assert_eq!( 22 | &e.render(r#"{{ basic("bob", "secret") }}"#), 23 | "Ym9iOnNlY3JldA==", 24 | "token should be issued now-ish" 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /curlz/src/curlz/template/functions/auth/jwt.rs: -------------------------------------------------------------------------------- 1 | use chrono::{Duration, Timelike, Utc}; 2 | use humantime::parse_duration; 3 | use jsonwebtoken::{EncodingKey, Header}; 4 | use minijinja::value::ValueKind; 5 | use minijinja::{value::Value, Error, ErrorKind, State}; 6 | use std::collections::hash_map::Entry; 7 | use std::collections::HashMap; 8 | use std::ops::Add; 9 | 10 | const CLAIM_EXPIRY: &str = "exp"; 11 | const CLAIM_ISSUED_AT: &str = "iat"; 12 | 13 | type ClaimsMap = HashMap; 14 | 15 | /// generates a jwt token based on some given claims 16 | pub(super) fn jwt( 17 | state: &State, 18 | claims: Value, 19 | jwt_signing_key: Option, 20 | ) -> Result { 21 | let mut claims_map = ClaimsMap::new(); 22 | for key in claims.try_iter()? { 23 | let value = claims.get_item(&key)?; 24 | 25 | if value.is_undefined() { 26 | // we should give it a try as json string 27 | let claims: HashMap = 28 | serde_json::from_str(claims.to_string().as_str()).map_err(|_| Error::new( 29 | ErrorKind::CannotUnpack, 30 | "The variable `claims` was not a valid json map nor consists of valid key-value arguments", 31 | ))?; 32 | claims.into_iter().for_each(|(k, v)| { 33 | claims_map.entry(k).or_insert(Value::from_serializable(&v)); 34 | }); 35 | 36 | break; 37 | } 38 | 39 | claims_map.insert(format!("{key}"), value); 40 | } 41 | 42 | prepare_claim_exp(&mut claims_map)?; 43 | prepare_claim_iat(&mut claims_map); 44 | 45 | let jwt_signing_key = jwt_signing_key 46 | .or_else(|| state.lookup("jwt_signing_key")) 47 | .or_else(|| claims_map.get("jwt_signing_key").map(|v| v.to_owned())) 48 | .ok_or_else(|| { 49 | Error::new( 50 | ErrorKind::MissingArgument, 51 | "The variable `jwt_signing_key` was not defined.", 52 | ) 53 | })?; 54 | // in any case we want the signing key never in the claims list 55 | claims_map.remove("jwt_signing_key"); 56 | 57 | let token = jsonwebtoken::encode( 58 | &Header::default(), 59 | &claims_map, 60 | &EncodingKey::from_secret(jwt_signing_key.as_bytes().ok_or(Error::new( 61 | ErrorKind::BadSerialization, 62 | "`jwt_signing_key` had an invalid datatype, it should be a string.", 63 | ))?), 64 | ); 65 | 66 | token.map_err(|e| { 67 | Error::new( 68 | ErrorKind::UndefinedError, 69 | "jsonwebtoken failed to encode the token.", 70 | ) 71 | .with_source(e) 72 | }) 73 | } 74 | 75 | fn prepare_claim_iat(claims_map: &mut HashMap) { 76 | claims_map.insert( 77 | CLAIM_ISSUED_AT.to_string(), 78 | Value::from(Utc::now().with_second(0).unwrap().timestamp()), 79 | ); 80 | } 81 | 82 | /// in case expiry is missing, the following values are supported: 83 | /// - strings parsable as human date format like "15min" 84 | /// - number of non-leap seconds since January 1, 1970 0:00:00 UTC (aka "UNIX timestamp") 85 | fn prepare_claim_exp(claims_map: &mut ClaimsMap) -> Result<(), Error> { 86 | let default_ts = || { 87 | Utc::now() 88 | .add(Duration::minutes(15)) 89 | .with_second(0) 90 | .unwrap() 91 | .timestamp() 92 | }; 93 | 94 | match claims_map.entry(CLAIM_EXPIRY.to_string()) { 95 | Entry::Occupied(mut exp) => { 96 | let exp_ts: i64 = match exp.get().kind() { 97 | ValueKind::Number => exp.get().clone().try_into().unwrap_or(default_ts()), 98 | ValueKind::String => { 99 | parse_duration(exp.get().as_str().unwrap()).map_err( 100 | |_| Error::new( 101 | ErrorKind::CannotUnpack, 102 | "claim `exp` has an invalid format it must be either a numerical duration or a duration string like '15min'", 103 | ) 104 | ).map(|std_duration| { 105 | // todo: this should not fail, except for some number overruns `i64` vs `u64` 106 | Duration::from_std(std_duration).map(|duration|{ 107 | Utc::now().add(duration).with_second(0).unwrap().timestamp() 108 | }).unwrap_or(default_ts()) 109 | })? 110 | } 111 | _ => default_ts(), 112 | }; 113 | 114 | exp.insert(Value::from(exp_ts)); 115 | } 116 | Entry::Vacant(vac) => { 117 | vac.insert(Value::from(default_ts())); 118 | } 119 | }; 120 | 121 | Ok(()) 122 | } 123 | 124 | #[cfg(test)] 125 | mod tests { 126 | use super::*; 127 | use crate::test_utils::RenderBuilder; 128 | use chrono::{Duration, Timelike}; 129 | use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; 130 | use rstest::rstest; 131 | use serde::Deserialize; 132 | 133 | const JWT_SECRET_KEY: &str = "000"; 134 | 135 | mod unhappy_path { 136 | use super::*; 137 | 138 | #[test] 139 | #[should_panic(expected = "The variable `jwt_signing_key` was not defined.")] 140 | fn should_throw_when_signing_key_is_missing() { 141 | RenderBuilder::new() 142 | .with_function("jwt", jwt) 143 | .render(r#"Bearer {{ jwt(sub="b@b.com") }}"#); 144 | } 145 | 146 | #[test] 147 | #[should_panic( 148 | expected = "`jwt_signing_key` had an invalid datatype, it should be a string." 149 | )] 150 | fn should_throw_when_signing_key_is_of_invalid_type() { 151 | RenderBuilder::new() 152 | .with_function("jwt", jwt) 153 | .render(r#"Bearer {{ jwt(sub="b@b.com", jwt_signing_key=12345) }}"#); 154 | } 155 | 156 | #[test] 157 | #[should_panic( 158 | expected = "The variable `claims` was not a valid json map nor consists of valid key-value arguments" 159 | )] 160 | fn should_throw_when_invalid_claims_are_provided() { 161 | RenderBuilder::new() 162 | .with_function("jwt", jwt) 163 | .render(r#"Bearer {{ jwt("b@b.com") }}"#); 164 | } 165 | 166 | #[test] 167 | #[should_panic( 168 | expected = "claim `exp` has an invalid format it must be either a numerical duration or a duration string like '15min'" 169 | )] 170 | fn should_throw_when_invalid_expire_date_are_provided() { 171 | RenderBuilder::new() 172 | .with_function("jwt", jwt) 173 | .with_env_var("key", JWT_SECRET_KEY) 174 | .render(r#"Bearer {{ jwt(exp="should not be string", jwt_signing_key=key) }}"#); 175 | } 176 | } 177 | 178 | #[rstest] 179 | #[case( 180 | r#"Bearer {{ jwt(sub="b@b.com", jwt_signing_key="000") }}"#, 181 | RenderBuilder::new().with_function("jwt", jwt) 182 | )] 183 | #[case( 184 | r#"Bearer {{ jwt(sub="b@b.com") }}"#, 185 | RenderBuilder::new().with_function("jwt", jwt) 186 | .with_env_var("jwt_signing_key", JWT_SECRET_KEY) 187 | )] 188 | #[case( 189 | r#"Bearer {{ jwt(sub="b@b.com", iat=666) }}"#, 190 | RenderBuilder::new().with_function("jwt", jwt) 191 | .with_env_var("jwt_signing_key", JWT_SECRET_KEY) 192 | )] 193 | #[case( 194 | r#"Bearer {{ jwt(jwt_claims) }}"#, 195 | RenderBuilder::new().with_function("jwt", jwt) 196 | .with_env_var("jwt_signing_key", JWT_SECRET_KEY) 197 | .with_env_var("jwt_claims", r#"{"sub": "b@b.com", "iat": 666}"#) 198 | )] 199 | fn should_set_expiry_when_missing(#[case] token: &str, #[case] builder: RenderBuilder) { 200 | let now = Utc::now(); 201 | let jwt = builder.render(token); 202 | 203 | let token_message = { 204 | #[derive(Deserialize)] 205 | struct Claims { 206 | sub: String, 207 | exp: i64, 208 | iat: i64, 209 | } 210 | 211 | let jwt = jwt.as_str().split(' ').last().unwrap(); 212 | decode::( 213 | jwt, 214 | &DecodingKey::from_secret(JWT_SECRET_KEY.as_bytes()), 215 | &Validation::new(Algorithm::HS256), 216 | ) 217 | .expect("decode claims failed") 218 | }; 219 | 220 | assert_eq!(token_message.claims.sub.as_str(), "b@b.com"); 221 | assert_eq!( 222 | token_message.claims.exp, 223 | now.add(Duration::minutes(15)) 224 | .with_second(0) 225 | .unwrap() 226 | .timestamp(), 227 | "token should expire in 15min" 228 | ); 229 | assert_eq!( 230 | token_message.claims.iat, 231 | now.with_second(0).unwrap().timestamp(), 232 | "token should be issued now-ish" 233 | ); 234 | } 235 | 236 | mod test_prepare_claim_exp { 237 | use super::*; 238 | 239 | #[test] 240 | fn test_default_value() { 241 | let mut hm = ClaimsMap::default(); 242 | prepare_claim_exp(&mut hm).unwrap(); 243 | assert_eq!( 244 | hm["exp"], 245 | Value::from( 246 | Utc::now() 247 | .add(Duration::minutes(15)) 248 | .with_second(0) 249 | .unwrap() 250 | .timestamp() 251 | ) 252 | ); 253 | } 254 | 255 | #[test] 256 | fn test_default_value_on_invalid() { 257 | let mut hm = ClaimsMap::default(); 258 | hm.insert("exp".to_string(), Value::from(vec!["a", "b"])); 259 | prepare_claim_exp(&mut hm).unwrap(); 260 | assert_eq!( 261 | hm["exp"], 262 | Value::from( 263 | Utc::now() 264 | .add(Duration::minutes(15)) 265 | .with_second(0) 266 | .unwrap() 267 | .timestamp() 268 | ) 269 | ); 270 | } 271 | 272 | #[test] 273 | fn test_string_parsing() { 274 | let mut hm = ClaimsMap::default(); 275 | hm.insert("exp".to_string(), "1h".into()); 276 | prepare_claim_exp(&mut hm).unwrap(); 277 | assert_eq!( 278 | hm["exp"], 279 | Value::from( 280 | Utc::now() 281 | .add(Duration::hours(1)) 282 | .with_second(0) 283 | .unwrap() 284 | .timestamp() 285 | ) 286 | ); 287 | } 288 | 289 | #[test] 290 | fn test_numerical_value() { 291 | let mut hm = ClaimsMap::default(); 292 | hm.insert("exp".to_string(), 1679575260_i64.into()); 293 | prepare_claim_exp(&mut hm).unwrap(); 294 | assert_eq!(hm["exp"], Value::from(1679575260_i64)); 295 | } 296 | 297 | #[test] 298 | fn test_numerical_value_of_other_type() { 299 | let mut hm = ClaimsMap::default(); 300 | hm.insert("exp".to_string(), 1679575260_u32.into()); 301 | prepare_claim_exp(&mut hm).unwrap(); 302 | assert_eq!(hm["exp"], Value::from(1679575260_i64)); 303 | } 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /curlz/src/curlz/template/functions/auth/mod.rs: -------------------------------------------------------------------------------- 1 | use minijinja::Environment; 2 | 3 | mod basic; 4 | mod jwt; 5 | 6 | use basic::basic; 7 | use jwt::jwt; 8 | 9 | pub fn register_functions(env: &mut Environment) { 10 | env.add_function("jwt", jwt); 11 | env.add_function("basic", basic); 12 | } 13 | -------------------------------------------------------------------------------- /curlz/src/curlz/template/functions/chrono/mod.rs: -------------------------------------------------------------------------------- 1 | use minijinja::Environment; 2 | 3 | mod timestamp; 4 | 5 | pub(super) fn register_functions(env: &mut Environment) { 6 | env.add_function("timestamp", timestamp::timestamp); 7 | } 8 | -------------------------------------------------------------------------------- /curlz/src/curlz/template/functions/chrono/timestamp.rs: -------------------------------------------------------------------------------- 1 | use chrono::Utc; 2 | use minijinja::value::Value; 3 | use minijinja::State; 4 | 5 | pub(super) fn timestamp(_: &State) -> Value { 6 | Value::from(Utc::now().timestamp()) 7 | } 8 | 9 | #[cfg(test)] 10 | mod tests { 11 | use super::*; 12 | use crate::test_utils::RenderBuilder; 13 | use chrono::Utc; 14 | 15 | #[test] 16 | fn test_timestamp() { 17 | let now = Utc::now(); 18 | let timestamp_result = RenderBuilder::new() 19 | .with_function("timestamp", timestamp) 20 | .render(r#"now = {{ timestamp() }}"#); 21 | 22 | assert_eq!(format!("now = {}", now.timestamp()), timestamp_result); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /curlz/src/curlz/template/functions/mod.rs: -------------------------------------------------------------------------------- 1 | use minijinja::value::Value; 2 | use minijinja::Environment; 3 | 4 | mod auth; 5 | mod chrono; 6 | mod process_env; 7 | mod prompt; 8 | 9 | pub(super) fn register_functions(env: &mut Environment) { 10 | env.add_function("processEnv", process_env::process_env); 11 | env.add_function("process_env", process_env::process_env); 12 | // this provides lazy env var lookup 13 | env.add_global("env", Value::from_struct_object(process_env::ProcessEnv)); 14 | 15 | prompt::register_functions(env); 16 | auth::register_functions(env); 17 | chrono::register_functions(env); 18 | } 19 | -------------------------------------------------------------------------------- /curlz/src/curlz/template/functions/process_env.rs: -------------------------------------------------------------------------------- 1 | use minijinja::value::{StructObject, Value}; 2 | use minijinja::{Error, State}; 3 | 4 | pub struct ProcessEnv; 5 | impl StructObject for ProcessEnv { 6 | fn get_field(&self, field: &str) -> Option { 7 | // std::env::var(var_name) 8 | // .map(|v| Value::from_safe_string(v)) 9 | // .map_err(|e| match e { 10 | // VarError::NotPresent => Error::new( 11 | // ErrorKind::NonKey, 12 | // format!("The process env variable `{var_name}` is not defined."), 13 | // ), 14 | // VarError::NotUnicode(_) => Error::new( 15 | // ErrorKind::UndefinedError, 16 | // format!("The process env variable `{var_name}` has an invalid unicode value."), 17 | // ), 18 | // }) 19 | std::env::var(field).map(Value::from_safe_string).ok() 20 | } 21 | } 22 | 23 | /// function for minijinja 24 | pub fn process_env(_: &State, var_name: &str) -> Result { 25 | Ok(ProcessEnv.get_field(var_name).unwrap_or_default()) 26 | } 27 | 28 | #[cfg(test)] 29 | mod tests { 30 | use crate::domain::environment::Environment; 31 | use crate::template::Renderer; 32 | use crate::test_utils::RenderBuilder; 33 | use std::collections::HashMap; 34 | 35 | use super::*; 36 | 37 | #[test] 38 | #[cfg(not(windows))] 39 | fn should_throw_when_var_name_is_missing() { 40 | assert_eq!( 41 | RenderBuilder::new() 42 | .with_function("processEnv", process_env) 43 | .render(r#"{{ processEnv("USER") }}"#), 44 | std::env::var("USER").unwrap() 45 | ); 46 | } 47 | 48 | #[test] 49 | #[cfg(windows)] 50 | fn should_throw_when_var_name_is_missing() { 51 | assert_eq!( 52 | RenderBuilder::new() 53 | .with_function("processEnv", process_env) 54 | .render(r#"{{ processEnv("USERNAME") }}"#), 55 | std::env::var("USERNAME").unwrap() 56 | ); 57 | } 58 | 59 | #[test] 60 | fn should_resolve_lazy_via_env_virtuell_object() { 61 | let mut r = Renderer::new(&Environment::default()); 62 | #[cfg(not(windows))] 63 | assert_eq!( 64 | r.render(r#"{{ env.USER }}"#, "template").unwrap(), 65 | std::env::var("USER").unwrap() 66 | ); 67 | #[cfg(windows)] 68 | assert_eq!( 69 | r.render(r#"{{ env.USERNAME }}"#, "template").unwrap(), 70 | std::env::var("USERNAME").unwrap() 71 | ); 72 | } 73 | 74 | #[test] 75 | fn should_provide_process_env_var_via_env_object() { 76 | // TODO: this is just a toy around case 77 | let mut hm = HashMap::new(); 78 | for (key, value) in std::env::vars() { 79 | println!("{key}: {value}"); 80 | hm.insert(key, value); 81 | } 82 | #[cfg(not(windows))] 83 | assert!(hm.contains_key("USER")); 84 | #[cfg(windows)] 85 | assert!(hm.contains_key("USERNAME")); 86 | 87 | let b = RenderBuilder::new().with_env_var("env", hm); 88 | 89 | #[cfg(not(windows))] 90 | assert_eq!( 91 | b.render(r#"{{ env.USER }}"#), 92 | std::env::var("USER").unwrap() 93 | ); 94 | #[cfg(windows)] 95 | assert_eq!( 96 | b.render(r#"{{ env.USERNAME }}"#), 97 | std::env::var("USERNAME").unwrap() 98 | ); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /curlz/src/curlz/template/functions/prompt.rs: -------------------------------------------------------------------------------- 1 | use dialoguer::{Input, Password}; 2 | use minijinja::value::Value; 3 | use minijinja::{Environment, Error, ErrorKind}; 4 | 5 | pub fn register_functions(env: &mut Environment) { 6 | env.add_function("prompt_password", prompt_password); 7 | env.add_function("prompt_for", prompt_for); 8 | } 9 | 10 | /// prompt for a password, to be used in a minijinja template 11 | fn prompt_password(_state: &minijinja::State) -> Result { 12 | Password::new() 13 | .with_prompt("Password") 14 | .allow_empty_password(true) 15 | .interact() 16 | .map_err(|e| { 17 | Error::new(ErrorKind::UndefinedError, "cannot read password from stdin").with_source(e) 18 | }) 19 | } 20 | 21 | /// prompt for something that has a name, to be used in a minijinja template 22 | fn prompt_for(_state: &minijinja::State, prompt: Value) -> Result { 23 | let prompt = prompt.to_string(); 24 | Input::new() 25 | .with_prompt(prompt) 26 | .allow_empty(true) 27 | .interact() 28 | .map_err(|e| { 29 | Error::new(ErrorKind::UndefinedError, "cannot read prompt from stdin").with_source(e) 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /curlz/src/curlz/template/mod.rs: -------------------------------------------------------------------------------- 1 | mod functions; 2 | pub mod variables; 3 | 4 | use crate::domain::environment::Environment; 5 | 6 | use minijinja::value::Value; 7 | use minijinja::Environment as MEnvironment; 8 | 9 | pub struct Renderer<'source> { 10 | env: MEnvironment<'source>, 11 | ctx: Value, 12 | } 13 | 14 | impl<'source> From<&Environment> for Renderer<'source> { 15 | fn from(env: &Environment) -> Self { 16 | Self::new(env) 17 | } 18 | } 19 | 20 | impl<'source> Renderer<'source> { 21 | pub fn new(env: &Environment) -> Self { 22 | let ctx: Value = env.into(); 23 | let mut env = MEnvironment::new(); 24 | 25 | functions::register_functions(&mut env); 26 | 27 | Self { env, ctx } 28 | } 29 | 30 | pub fn inject_variable(&mut self, p0: &'source str, p1: String) { 31 | self.env.add_global(p0, Value::from_safe_string(p1)); 32 | } 33 | 34 | pub fn render(&mut self, str: &'source str, name: &'source str) -> crate::Result { 35 | self.env.add_template(name, str)?; 36 | let template = self.env.get_template(name)?; 37 | 38 | template.render(&self.ctx).map_err(|e| e.into()) 39 | } 40 | } 41 | 42 | #[cfg(test)] 43 | mod tests { 44 | use super::*; 45 | 46 | #[test] 47 | fn test_renderer_from_environment() { 48 | let mut env = Environment::default(); 49 | env.insert("foo", "bar"); 50 | let mut r: Renderer = (&env).into(); 51 | r.inject_variable("bak", "foo".to_string()); 52 | 53 | assert_eq!(r.render("{{ foo }}", "something").unwrap(), "bar"); 54 | assert_eq!(r.render("{{ bak }}", "something2").unwrap(), "foo"); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /curlz/src/curlz/template/variables.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 4 | pub struct Placeholder { 5 | pub name: String, 6 | pub value: Option, 7 | pub default: Option, 8 | pub prompt: Option, 9 | } 10 | 11 | impl Placeholder { 12 | pub fn new(key: impl AsRef, value: impl AsRef) -> Self { 13 | Self { 14 | name: key.as_ref().to_owned(), 15 | value: value.as_ref().to_owned().into(), 16 | default: None, 17 | prompt: None, 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /curlz/src/curlz/test_utils.rs: -------------------------------------------------------------------------------- 1 | use minijinja::functions::Function; 2 | use minijinja::value::{FunctionArgs, FunctionResult, Value}; 3 | use minijinja::Environment; 4 | use std::borrow::Cow; 5 | use std::fs::File; 6 | use std::io::{Read, Write}; 7 | use std::ops::Not; 8 | use std::path::Path; 9 | use tempfile::TempDir; 10 | 11 | pub mod sample_requests { 12 | use crate::domain::http::*; 13 | use indoc::indoc; 14 | 15 | pub fn post_request() -> HttpRequest { 16 | HttpRequest { 17 | url: "https://httpbin.org/anything".into(), 18 | method: HttpMethod::Post, 19 | version: HttpVersion::Http11, 20 | headers: HttpHeaders::from( 21 | [ 22 | "Accept: application/json".to_owned(), 23 | "Content-Type: application/json".to_owned(), 24 | ] 25 | .as_slice(), 26 | ), 27 | body: HttpBody::InlineText( 28 | indoc! {r#" 29 | { 30 | "foo": "Bar", 31 | "bool": true 32 | } 33 | "#} 34 | .to_owned(), 35 | ), 36 | curl_params: Default::default(), 37 | placeholders: Default::default(), 38 | } 39 | } 40 | } 41 | 42 | /// [`RenderBuilder`] simplifies test case creation 43 | pub struct RenderBuilder<'source> { 44 | env: Environment<'source>, 45 | } 46 | 47 | impl<'source> RenderBuilder<'source> { 48 | pub fn with_env_var(mut self, name: N, value: impl Into) -> Self 49 | where 50 | N: Into>, 51 | { 52 | self.env.add_global(name, value.into()); 53 | 54 | self 55 | } 56 | } 57 | 58 | impl<'source> Default for RenderBuilder<'source> { 59 | fn default() -> Self { 60 | Self { 61 | env: Environment::empty(), 62 | } 63 | } 64 | } 65 | 66 | impl<'source> RenderBuilder<'source> { 67 | /// creates a new fresh builder 68 | pub fn new() -> Self { 69 | Self::default() 70 | } 71 | 72 | /// registers a template filter function 73 | pub fn with_function(mut self, name: &'source str, f: F) -> Self 74 | where 75 | // the crazy bounds here exist to enable borrowing in closures 76 | F: Function + for<'a> Function>::Output>, 77 | Rv: FunctionResult, 78 | Args: for<'a> FunctionArgs<'a>, 79 | { 80 | self.env.add_function(name, f); 81 | 82 | self 83 | } 84 | 85 | /// registers an object as e.g. global object 86 | pub fn with_object(mut self, name: N, value: V) -> Self 87 | where 88 | N: Into>, 89 | V: Into, 90 | { 91 | self.env.add_global(name.into(), value.into()); 92 | 93 | self 94 | } 95 | 96 | /// it renders a given template 97 | pub fn render(mut self, template: &'source str) -> String { 98 | let name = "render-builder-template"; 99 | self.env.add_template(name, template).unwrap(); 100 | let template = self.env.get_template(name).unwrap(); 101 | 102 | let ctx = Value::default(); 103 | template.render(&ctx).unwrap() 104 | } 105 | } 106 | 107 | pub fn create_file( 108 | filename: impl AsRef, 109 | contents: impl AsRef, 110 | ) -> anyhow::Result { 111 | let base_path = tempfile::tempdir()?; 112 | let path = base_path.path().join(filename.as_ref()); 113 | 114 | if let Some(parent) = path.parent() { 115 | if parent != base_path.path() { 116 | std::fs::create_dir_all(parent)?; 117 | } 118 | } 119 | 120 | let mut file = File::create(&path)?; 121 | file.write_all(contents.as_ref().as_ref())?; 122 | 123 | Ok(base_path) 124 | } 125 | 126 | #[cfg(test)] 127 | mod tests { 128 | use super::*; 129 | 130 | #[test] 131 | fn test_create_file_cleanup() { 132 | let path = { 133 | let tmp = create_file("foobar", "Hello").unwrap(); 134 | tmp.path() 135 | .join("foobar") 136 | .as_path() 137 | .to_str() 138 | .unwrap() 139 | .to_string() 140 | }; 141 | assert!(Path::exists(Path::new(&path)).not()); 142 | } 143 | 144 | #[test] 145 | fn test_create_file() { 146 | let tmp = create_file("foobar", "Hello").unwrap(); 147 | 148 | let mut hello = String::new(); 149 | let mut f = File::open(tmp.path().join("foobar").as_path()).unwrap(); 150 | f.read_to_string(&mut hello).unwrap(); 151 | assert_eq!(hello.as_str(), "Hello"); 152 | } 153 | 154 | #[test] 155 | fn test_create_file_with_dir() { 156 | let tmp = create_file("foo/foobar", "Hello").unwrap(); 157 | 158 | let mut hello = String::new(); 159 | let mut f = File::open(tmp.path().join("foo").join("foobar").as_path()).unwrap(); 160 | f.read_to_string(&mut hello).unwrap(); 161 | assert_eq!(hello.as_str(), "Hello"); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /curlz/src/curlz/utils.rs: -------------------------------------------------------------------------------- 1 | /// parses pairs like `"key=value"` strings into tuples of `Option<(key, value)>` 2 | /// spaces around the `separator` are being removed 3 | #[inline] 4 | pub fn parse_pairs(pairs: &str, separator: char) -> Option<(&str, &str)> { 5 | pairs 6 | .split_once(separator) 7 | .map(|(key, value)| (key.trim(), value.trim())) 8 | } 9 | 10 | #[cfg(test)] 11 | mod tests { 12 | use super::*; 13 | 14 | #[test] 15 | fn should_split_defines_by_equal_happy_path() { 16 | assert_eq!(parse_pairs("foo=bar", '='), Some(("foo", "bar"))); 17 | assert_eq!(parse_pairs("foo =bar", '='), Some(("foo", "bar"))); 18 | assert_eq!(parse_pairs("foo = bar", '='), Some(("foo", "bar"))); 19 | assert_eq!(parse_pairs("foo= bar", '='), Some(("foo", "bar"))); 20 | assert_eq!(parse_pairs("foo: bar", ':'), Some(("foo", "bar"))); 21 | } 22 | 23 | #[test] 24 | fn should_split_defines_by_equal_unhappy_path() { 25 | assert_eq!( 26 | parse_pairs("baz=123324+adf+=vasdf", '='), 27 | Some(("baz", "123324+adf+=vasdf")) 28 | ); 29 | } 30 | 31 | #[test] 32 | fn should_not_split_defines_if_no_equal_is_contained() { 33 | assert_eq!(parse_pairs("foo", '='), None); 34 | assert_eq!(parse_pairs("baz=", '='), Some(("baz", ""))); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /curlz/tests/basics.rs: -------------------------------------------------------------------------------- 1 | use assert_cmd::prelude::*; 2 | use curlz::domain::http::{HttpBody, HttpMethod}; 3 | use predicates::prelude::*; 4 | 5 | use crate::testlib::{binary, CurlzTestSuite}; 6 | 7 | mod testlib; 8 | 9 | #[test] 10 | fn should_show_usage_when_no_args_passed() { 11 | #[cfg(windows)] 12 | let pattern = predicate::str::contains("Usage: curlz.exe [OPTIONS] "); 13 | #[cfg(not(windows))] 14 | let pattern = predicate::str::contains("Usage: curlz [OPTIONS] "); 15 | 16 | binary().assert().failure().stderr(pattern); 17 | } 18 | 19 | #[tokio::test] 20 | async fn should_send_as_get() { 21 | CurlzTestSuite::new() 22 | .with_path("/gitignore/templates/Rust") 23 | .send_request() 24 | .await; 25 | } 26 | 27 | #[tokio::test] 28 | async fn should_send_as_post() { 29 | CurlzTestSuite::new() 30 | .with_path("/post") 31 | .with_method(HttpMethod::Post) 32 | .send_request() 33 | .await; 34 | } 35 | 36 | #[tokio::test] 37 | async fn should_send_text_as_put() { 38 | CurlzTestSuite::new() 39 | .with_path("/put") 40 | .with_method(HttpMethod::Put) 41 | .with_payload(HttpBody::InlineText("Howdy Pal!".to_string())) 42 | .send_request() 43 | .await; 44 | } 45 | 46 | #[tokio::test] 47 | async fn should_send_as_post_with_body_variables() { 48 | CurlzTestSuite::new() 49 | .with_env_variable("id", "1") 50 | .with_env_variable("username", "john") 51 | .with_path("/post") 52 | .with_method(HttpMethod::Post) 53 | .with_payload(r#"{ "id": {{ id }}, "user": "{{ username }}" }"#) 54 | .expect_payload(predicate::str::contains(r#"{ "id": 1, "user": "john" }"#)) 55 | .send_request() 56 | .await; 57 | } 58 | -------------------------------------------------------------------------------- /curlz/tests/fixtures/send-payload-as-post.http: -------------------------------------------------------------------------------- 1 | ### this is a POST request with a body 2 | POST https://httpbin.org/anything 3 | Accept: application/json 4 | Content-Type: application/json 5 | 6 | { 7 | "foo": "Bar", 8 | "bool": true 9 | } -------------------------------------------------------------------------------- /curlz/tests/testlib.rs: -------------------------------------------------------------------------------- 1 | use assert_cmd::prelude::*; 2 | use std::collections::HashMap; 3 | use std::ops::Deref; 4 | 5 | use assert_cmd::assert::Assert; 6 | use dotenvy::dotenv; 7 | use predicates::str::contains; 8 | use predicates::{BoxPredicate, Predicate}; 9 | use std::process::Command; 10 | use wiremock::matchers::{method, path}; 11 | use wiremock::{Mock, MockServer, Request, Respond, ResponseTemplate}; 12 | 13 | use curlz::domain::http::{HttpBody, HttpMethod}; 14 | 15 | pub fn binary() -> Command { 16 | Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap() 17 | } 18 | 19 | pub struct CurlzTestSuite { 20 | url_part: String, 21 | http_method: String, 22 | payload: HttpBody, 23 | defined_variables: HashMap, 24 | expected_stdout: BoxPredicate, 25 | } 26 | 27 | impl Default for CurlzTestSuite { 28 | fn default() -> Self { 29 | dotenv().ok(); 30 | Self { 31 | url_part: "/".to_string(), 32 | http_method: "GET".to_string(), 33 | payload: HttpBody::None, 34 | defined_variables: Default::default(), 35 | expected_stdout: BoxPredicate::new(contains("")), 36 | } 37 | } 38 | } 39 | 40 | impl CurlzTestSuite { 41 | pub fn new() -> Self { 42 | Self::default() 43 | } 44 | 45 | /// runs curlz and requests the given url from a local echo http server 46 | pub async fn send_request(mut self) -> Assert { 47 | let mock_server = self.prepare_mock_server().await; 48 | 49 | binary() 50 | .arg("r") 51 | .args(self.args_method()) 52 | .args(self.args_define()) 53 | .args(self.args_data()) 54 | .arg(self.arg_url(&mock_server)) 55 | .assert() 56 | .success() 57 | .stdout(self.expected_stdout) 58 | .stderr(contains("% Total")) 59 | } 60 | 61 | /// prepares a variable that ends as a `--define name=value` cli argument 62 | pub fn with_env_variable(mut self, name: &str, value: &str) -> Self { 63 | self.defined_variables 64 | .insert(name.to_string(), value.to_string()); 65 | self 66 | } 67 | 68 | /// sets the target url to be requested 69 | pub fn with_path(mut self, url_part: &str) -> Self { 70 | self.url_part = url_part.to_string(); 71 | self 72 | } 73 | 74 | /// sets the http method used for the request 75 | pub fn with_method(mut self, http_method: HttpMethod) -> Self { 76 | self.http_method = (&http_method).into(); 77 | self 78 | } 79 | 80 | /// sets the http body payload that is send 81 | /// also readjusts the expected output based on the given payload 82 | pub fn with_payload(mut self, body: impl Into) -> Self { 83 | self.payload = body.into(); 84 | let predicate = { 85 | let pattern = String::from_utf8_lossy(self.payload.as_bytes().unwrap()); 86 | contains(pattern.deref()) 87 | }; 88 | 89 | self.expect_payload(predicate) 90 | } 91 | 92 | /// sets the expected output 93 | pub fn expect_payload>(mut self, predicate: P) -> Self 94 | where 95 | P: Send + Sync + 'static, 96 | { 97 | self.expected_stdout = BoxPredicate::new(predicate); 98 | self 99 | } 100 | 101 | fn args_method(&self) -> [&str; 2] { 102 | ["-X", self.http_method.as_str()] 103 | } 104 | 105 | fn arg_url(&self, mock_server: &MockServer) -> String { 106 | format!("{}{}", mock_server.uri(), self.url_part) 107 | } 108 | 109 | fn args_data(&self) -> Vec<&str> { 110 | match &self.payload { 111 | HttpBody::None => vec![], 112 | HttpBody::InlineText(s) => vec!["-d", s.as_str()], 113 | HttpBody::InlineBinary(_) => { 114 | todo!("binary data are not yet supported for the http body") 115 | } 116 | HttpBody::Extern(_) => { 117 | todo!("external file references are not yet supported for the http body") 118 | } 119 | } 120 | } 121 | 122 | fn args_define(&self) -> Vec { 123 | self.defined_variables 124 | .iter() 125 | .flat_map(|(name, value)| vec!["--define".to_string(), format!("{name}={value}")]) 126 | .collect::>() 127 | } 128 | 129 | async fn prepare_mock_server(&mut self) -> MockServer { 130 | let mock_server = MockServer::start().await; 131 | 132 | Mock::given(method(self.http_method.as_str())) 133 | .and(path(self.url_part.as_str())) 134 | .respond_with(EchoResponder::default()) 135 | .mount(&mock_server) 136 | .await; 137 | 138 | mock_server 139 | } 140 | } 141 | 142 | #[derive(Default)] 143 | struct EchoResponder; 144 | 145 | impl Respond for EchoResponder { 146 | fn respond(&self, request: &Request) -> ResponseTemplate { 147 | ResponseTemplate::new(200).set_body_bytes(request.body.as_slice()) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM curlimages/curl:8.00.1 as builder 2 | 3 | LABEL org.opencontainers.image.source https://github.com/curlz-rs/curlz 4 | ARG CURLZ_RELEASE_TAG=v0.1.0-alpha.11 5 | ARG CURLZ_GIT_REPO=https://github.com/curlz-rs/curlz 6 | ARG LABEL_VERSION=1.0.0 7 | ARG LABEL_NAME=curlz 8 | ARG LABEL_DESC=curlz 9 | 10 | WORKDIR /tmp 11 | RUN curl ${CURLZ_GIT_REPO}/releases/download/${CURLZ_RELEASE_TAG}/curlz-${CURLZ_RELEASE_TAG}-x86_64-unknown-linux-musl.tar.gz \ 12 | -L -o /tmp/curlz.tar.gz && \ 13 | tar xzfv /tmp/curlz.tar.gz curlz && \ 14 | chmod a+x /tmp/curlz 15 | 16 | FROM curlimages/curl:8.00.1 17 | 18 | COPY --from=builder "/tmp/curlz" "/usr/bin/curlz" 19 | USER curl_user 20 | 21 | COPY "entrypoint.sh" "/entrypoint.sh" 22 | CMD ["curlz"] 23 | ENTRYPOINT ["/entrypoint.sh"] 24 | -------------------------------------------------------------------------------- /docker/Makefile: -------------------------------------------------------------------------------- 1 | export LATEST_RELEASE_VERSION?=$(LATEST_RELEASE_VERSION) 2 | export LATEST_RELEASE_TAG:=v${LATEST_RELEASE_VERSION} 3 | 4 | # set docker build options used when building docker images 5 | export DOCKER_BUILD_OPTS:=--no-cache --compress 6 | 7 | # set docker build args used when building docker images 8 | export DOCKER_BUILD_ARGS:=--build-arg CURLZ_RELEASE_TAG=${LATEST_RELEASE_TAG} \ 9 | --build-arg CURLZ_RELEASE_VERSION=${LATEST_RELEASE_VERSION} 10 | 11 | export DOCKER_MULTI_ARCH:=linux/arm/v7,linux/amd64,linux/arm64,linux/ppc64le,linux/s390x,linux/386 12 | 13 | login: 14 | echo ${GITHUB_TOKEN} | docker login ghcr.io -u sassman --password-stdin 15 | 16 | build: 17 | docker build ${DOCKER_BUILD_OPTS} ${DOCKER_BUILD_ARGS} -t "ghcr.io/curlz-rs/curlz:${LATEST_RELEASE_VERSION}" -f Dockerfile . 18 | 19 | multibuild: 20 | docker buildx build ${DOCKER_BUILD_OPTS} ${DOCKER_BUILD_ARGS} -t "ghcr.io/curlz-rs/curlz:${LATEST_RELEASE_VERSION}" --platform=${DOCKER_MULTI_ARCH} -f Dockerfile . --push 21 | 22 | publish: 23 | docker push ghcr.io/curlz-rs/curlz:${LATEST_RELEASE_VERSION} 24 | 25 | test: 26 | docker run --rm ghcr.io/curlz-rs/curlz:${LATEST_RELEASE_VERSION} r https://httpbin.org/get 27 | -------------------------------------------------------------------------------- /docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | if [ "${1#-}" != "${1}" ] || [ -z "$(command -v "${1}")" ]; then 5 | set -- curlz "$@" 6 | fi 7 | 8 | exec "$@" 9 | -------------------------------------------------------------------------------- /docs/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Sven Kanoldt"] 3 | language = "en" 4 | multilingual = false 5 | title = "The curlz cookbook" 6 | description = "curl wrapper with placeholder, bookmark and environment powers just like postman" 7 | 8 | [output.html] 9 | git-repository-url = "https://github.com/curlz-rs/curlz" 10 | edit-url-template = "https://github.com/curlz-rs/curlz/edit/main/docs/{path}" 11 | site-url = "/curlz/" 12 | -------------------------------------------------------------------------------- /docs/src/README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | ## Features 4 | 5 | ### Placeholders everywhere 6 | 7 | ️placeholders at the beginning of an url e.g. 8 | 9 | ```sh 10 | curlz r --define 'host=https://httpbin.org' '{{host}}/get' 11 | ```` 12 | 13 | placeholders in HTTP Headers, e.g. 14 | 15 | ```sh 16 | curlz r -H 'Username: {{ env.USER }}' https://httpbin.org/headers 17 | ``` 18 | 19 | ### JSON Payload | `--json` 20 | 21 | This is a shortcut for setting 2 HTTP Headers and sending data as with `-d | --data` 22 | 23 | #### Example 24 | 25 | ```sh 26 | curlz r --json '{ "foo": "bar" }' -X POST 'https://httpbin.org/anything' 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | [Introduction](README.md) 4 | 5 | - [Template Functions](template-functions.md) 6 | -------------------------------------------------------------------------------- /docs/src/template-functions.md: -------------------------------------------------------------------------------- 1 | # Template function documentation 2 | 3 | ## Prompt User Input - `prompt_for(name: string)` 4 | 5 | - arguments: 6 | - `name`: a name that is printed before the user would input data 7 | - output: string 8 | - notes: 9 | - don't use this for passwords, consider `prompt_password()` for this 10 | - examples: 11 | - let the user enter an arbitrary username 12 | 13 | ```shell 14 | curlz r https://api.github.com/user -- \ 15 | -u '{{ prompt_for("GitHub Username") }}:{{ prompt_password() }}' 16 | ``` 17 | 18 | ## Prompt User for Password - `prompt_password()` 19 | 20 | - arguments: None 21 | - output: string 22 | 23 | ### Example: Username 24 | 25 | let the user enter an arbitrary username: 26 | 27 | ```sh 28 | curlz r -u '{{ prompt_for("GitHub Username") }}:{{ prompt_password() }}' https://api.github.com/user 29 | ``` 30 | 31 | ## Json Web Token - `jwt(claims: map, [jwt_signing_key: string])` 32 | 33 | - arguments: 34 | - `claims`: to be a map of key value pairs like `{"uid": "1234"}` that 35 | are the payload of the JWT 36 | - `jwt_signing_key`: to be a string, 37 | this is optional and when omitted a variable 38 | named `jwt_signing_key` will be taken from the `.env` file, 39 | if that variable is missing an error is raised 40 | - Note: also key value arguments are possible: `jwt(uid="1234", jwt_signing_key="999")` 41 | - output: string is a Json Web Token (JWT) 42 | - notes: 43 | - the hash algorithm is `HS256` and the JWT header is `{"alg": "HS256", "typ": "JWT"}` 44 | - the claim `exp` expiry timestamp (UNIX-Timestamp) is set to expire in 15min by default 45 | - can be overwritten by a UNIX-Timestamp or a duration string like "15min" or "1h" 46 | - the claim `iat` issued at timestamp is set automatically, can't be overwritten 47 | - you can find a full [list of commonly used claims at iana.org](https://www.iana.org/assignments/jwt/jwt.xhtml) 48 | 49 | ### Example: Simple Claims 50 | 51 | This example illustrates how a JWT Signing Key will be used from a `.env` file implicitly. 52 | Given an `.env` file like this: 53 | 54 | ```plain 55 | # .env 56 | jwt_signing_key=123Secret123 57 | ``` 58 | 59 | The usage of `jwt` would look like this: 60 | 61 | ```sh 62 | curlz r -H 'Authorization: Bearer {{ jwt({"email": "john@dow.com", "sub": "some subject"}) }}' https://httpbin.org/headers 63 | ``` 64 | 65 | Alternatively, you can provide the claims as named arguments like: 66 | 67 | ```sh 68 | curlz r -H 'Authorization: Bearer {{ jwt(email="john@dow.com", sub="some subject") }}' https://httpbin.org/headers 69 | 70 | { 71 | "headers": { 72 | "Accept": "*/*", 73 | "Authorization": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2Nzk1MjQ2ODAsInN1YiI6InNvbWUgc3ViamVjdCIsImVtYWlsIjoiam9obkBkb3cuY29tIiwiaWF0IjoxNjc5NTIzNzgwfQ.bt6taB1YGyMc_43mZeq77dgS_teyglUWtr1dsObyXTg", 74 | "Host": "httpbin.org", 75 | "User-Agent": "curl/7.86.0", 76 | "X-Amzn-Trace-Id": "Root=1-641b7fd7-65e24681239a404d6a143187" 77 | } 78 | } 79 | ``` 80 | 81 | ## Basic Auth Token - `basic(username: string, password: string)` 82 | 83 | - arguments: 84 | - `username`: the username as string 85 | - `password`: the password as string 86 | - output: string is a base64 encoded credential `username:password` 87 | 88 | ### Example 89 | 90 | send a basic auth header with `username` `joe` and `password` `secret`: 91 | 92 | ```sh 93 | curlz r -H 'Authorization: Basic {{ basic("joe", "secret") }}' https://httpbin.org/headers 94 | 95 | { 96 | "headers": { 97 | "Accept": "*/*", 98 | "Authorization": "Basic am9lOnNlY3JldA==", 99 | "Host": "httpbin.org", 100 | "User-Agent": "curl/7.86.0", 101 | "X-Amzn-Trace-Id": "Root=1-641b7fc0-1228f6242d06665a515e5cf9" 102 | } 103 | } 104 | ``` 105 | 106 | ## Unix Timestamp - `timestamp()` 107 | 108 | Returns the number of non-leap seconds since January 1, 1970 0:00:00 UTC (aka “UNIX timestamp”) 109 | 110 | ### Example of Timestamp 111 | 112 | ```shell 113 | curlz r -H 'Now: {{ timestamp() }}' https://httpbin.org/headers 114 | 115 | { 116 | "headers": { 117 | "Accept": "*/*", 118 | "Host": "httpbin.org", 119 | "Now": "1679678951", 120 | "User-Agent": "curl/7.86.0", 121 | "X-Amzn-Trace-Id": "Root=1-641ddde8-6f9dfa095fa782a323fc6300" 122 | } 123 | } 124 | ``` 125 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | Note: for testing you can `cargo install echo-server` and run `echo-server` 4 | 5 | - a request with a JWT authorization header, that has custom claims 6 | ```sh 7 | curlz r 'http://localhost:8080/' -- -vvv --header 'authorization: Baerer {{ jwt({"foo": "bar"}) }}' 8 | ``` -------------------------------------------------------------------------------- /examples/http-file/README.md: -------------------------------------------------------------------------------- 1 | # Feature `x-http-lang` 2 | 3 | In this folder you find examples that can be used for the curlz feature `x-http-lang`. 4 | 5 | ## How to execute 6 | 7 | - run `post-to-httpbin.http`: 8 | ```sh 9 | cargo run --features x-http-lang -- http-file ./post-to-httpbin.http 10 | ``` 11 | - run `basic-auth-header.http`: 12 | ```sh 13 | cargo run --features x-http-lang -- http-file ./basic-auth-header.http 14 | ``` -------------------------------------------------------------------------------- /examples/http-file/basic-auth-header.http: -------------------------------------------------------------------------------- 1 | ### sending a basic auth token via Authorization-Header 2 | GET https://httpbin.org/headers HTTP/1.1 3 | Authorization: Basic {{ basic("joe", "secret") }} 4 | 5 | ### sending a basic auth token via Authorization-Header 6 | 7 | GET https://httpbin.org/headers HTTP/1.1 8 | Authorization: Basic {{ basic("joe", "secret") }} 9 | -------------------------------------------------------------------------------- /examples/http-file/post-to-httpbin.http: -------------------------------------------------------------------------------- 1 | ### this is a POST request with a body 2 | POST https://httpbin.org/anything HTTP/1.1 3 | Accept: application/json 4 | Content-Type: application/json 5 | 6 | { 7 | "foo": "Bar", 8 | "bool": true 9 | } -------------------------------------------------------------------------------- /examples/http-file/with_trailing_whitespaces.http: -------------------------------------------------------------------------------- 1 | ### this is a POST request with a body 2 | POST https://httpbin.org/anything HTTP/1.1 3 | Accept: application/json 4 | Content-Type: application/json 5 | 6 | { 7 | "foo": "Bar", 8 | "bool": true 9 | } 10 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ], 6 | "separateMinorPatch": true, 7 | "minor": { 8 | "groupName": "bump minor versions" 9 | }, 10 | "patch": { 11 | "groupName": "bump patch versions" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /resources/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/curlz-rs/curlz/1d18d4a7f6bf606add23917c8765d504361c81e8/resources/demo.gif --------------------------------------------------------------------------------