├── .github
├── FUNDING.yml
└── workflows
│ ├── cd.yml
│ └── ci.yml
├── .gitignore
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── README.md
├── docs
└── logo.svg
├── examples
├── github-create-release.yaml
├── github-upload-release-asset.yaml
├── postman-echo-delete.yaml
├── postman-echo-get.yaml
├── postman-echo-patch.yaml
├── postman-echo-post-file.yaml
├── postman-echo-post.yaml
├── postman-echo-put.yaml
├── postman-echo-stream.yaml
├── request.yaml
├── request_html.yaml
├── request_with_advanced_template.yaml
├── request_with_env.yaml
├── request_with_file.yaml
├── request_with_params.yaml
├── request_with_query_params.yaml
└── request_yaml.yaml
├── rustfmt.toml
└── src
├── build_args.rs
├── dialog.rs
├── display.rs
├── editor.rs
├── execute.rs
├── http_utils.rs
├── import.rs
├── main.rs
├── manifests
├── config.rs
└── mod.rs
├── match_params.rs
├── match_prompts.rs
├── progress_component.rs
├── requests.rs
├── template.rs
└── validators.rs
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [ecyrbe]
4 | custom: ["https://www.paypal.me/ecyrbe"]
5 |
--------------------------------------------------------------------------------
/.github/workflows/cd.yml:
--------------------------------------------------------------------------------
1 | name: CD
2 | on:
3 | push:
4 | tags:
5 | - v*
6 | jobs:
7 | release:
8 | name: build linux
9 | strategy:
10 | fail-fast: false
11 | matrix:
12 | job:
13 | - os: ubuntu-latest
14 | target: x86_64-unknown-linux-gnu
15 | outputs:
16 | release: ${{ fromJson(steps.release.outputs.release).id }}
17 | env:
18 | ASSET: apix-${{github.ref_name}}-${{ matrix.job.target }}.tar.gz
19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
20 | runs-on: ${{ matrix.job.os }}
21 | steps:
22 | - name: checkout
23 | uses: actions/checkout@v2
24 | with:
25 | fetch-depth: 0
26 | - name: install toolchain
27 | uses: actions-rs/toolchain@v1
28 | with:
29 | toolchain: stable
30 | target: ${{ matrix.job.target }}
31 | profile: minimal
32 | default: true
33 | - name: install git-cliff
34 | uses: actions-rs/cargo@v1
35 | with:
36 | command: install
37 | args: git-cliff
38 | - name: build apix
39 | uses: actions-rs/cargo@v1
40 | env:
41 | RUSTFLAGS: -C link-arg=-s
42 | with:
43 | command: build
44 | args: --release
45 | # eat your own food. use apix to create a release
46 | - name: create release
47 | id: release
48 | run: |-
49 | git-cliff -l > changelog.txt
50 | export CHANGELOG=`cat changelog.txt`
51 | chmod +x target/release/apix
52 | cd examples
53 | ../target/release/apix exec github-create-release > release.json
54 | content=`cat release.json`
55 | content="${content//'%'/'%25'}"
56 | content="${content//$'\n'/'%0A'}"
57 | content="${content//$'\r'/'%0D'}"
58 | echo "::set-output name=release::$content"
59 | - name: create tarball
60 | run: |-
61 | cp README.md target/release/README.md
62 | cp LICENSE target/release/LICENSE
63 | cd target/release
64 | tar -zcvf ${{ env.ASSET }} apix LICENSE README.md
65 | - name: upload tarball
66 | env:
67 | RELEASE_ID: ${{ fromJson(steps.release.outputs.release).id }}
68 | run: |-
69 | cd examples
70 | ../target/release/apix exec github-upload-release-asset
71 | build:
72 | strategy:
73 | fail-fast: false
74 | matrix:
75 | job:
76 | - os: windows-latest
77 | target: x86_64-pc-windows-gnu
78 | ext: .zip
79 | compress: 7z -y a
80 | apix: apix.exe
81 | - os: windows-latest
82 | target: x86_64-pc-windows-msvc
83 | ext: .zip
84 | compress: 7z -y a
85 | apix: apix.exe
86 | - os: macos-latest
87 | target: x86_64-apple-darwin
88 | ext: .tar.gz
89 | compress: tar -zcvf
90 | apix: apix
91 | runs-on: ${{ matrix.job.os }}
92 | needs:
93 | - release
94 | env:
95 | ASSET: apix-${{github.ref_name}}-${{ matrix.job.target }}${{ matrix.job.ext }}
96 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
97 | steps:
98 | - name: checkout
99 | uses: actions/checkout@v2
100 | - name: install toolchain
101 | uses: actions-rs/toolchain@v1
102 | with:
103 | toolchain: stable
104 | profile: minimal
105 | target: ${{ matrix.job.target }}
106 | default: true
107 | - name: build apix
108 | uses: actions-rs/cargo@v1
109 | env:
110 | RUSTFLAGS: -C link-arg=-s
111 | with:
112 | command: build
113 | args: --release
114 | - name: create artifact
115 | run: |-
116 | cp README.md target/release/README.md
117 | cp LICENSE target/release/LICENSE
118 | cd target/release
119 | chmod +x ${{ matrix.job.apix }}
120 | ${{ matrix.job.compress }} ${{ env.ASSET}} ${{ matrix.job.apix }} LICENSE README.md
121 | - name: upload artifact
122 | env:
123 | RELEASE_ID: ${{ needs.release.outputs.release }}
124 | run: |-
125 | cd examples
126 | ../target/release/${{ matrix.job.apix }} exec github-upload-release-asset
127 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | push:
4 | branches:
5 | - main
6 | paths:
7 | - src/**
8 | - Cargo.lock
9 | - Cargo.toml
10 | pull_request:
11 | branches:
12 | - main
13 | paths:
14 | - src/**
15 | - Cargo.lock
16 | - Cargo.toml
17 | env:
18 | CARGO_TERM_COLOR: always
19 | jobs:
20 | ci:
21 | strategy:
22 | fail-fast: false
23 | matrix:
24 | config:
25 | - do: check format clippy test
26 | os: ubuntu-latest
27 | - do: check test
28 | os: windows-latest
29 | - do: check test
30 | os: macos-latest
31 | runs-on: ${{ matrix.config.os }}
32 | steps:
33 | - name: Checkout
34 | uses: actions/checkout@v2
35 | - name: install toolchain
36 | uses: actions-rs/toolchain@v1
37 | with:
38 | toolchain: stable
39 | profile: minimal
40 | components: clippy, rustfmt
41 | default: true
42 | - name: check code
43 | if: contains(matrix.config.do, 'check')
44 | uses: actions-rs/cargo@v1
45 | with:
46 | command: check
47 | - name: check format
48 | if: contains(matrix.config.do, 'format')
49 | uses: actions-rs/cargo@v1
50 | with:
51 | command: fmt
52 | args: --all -- --check
53 | - name: static code analysis
54 | if: contains(matrix.config.do, 'clippy')
55 | uses: actions-rs/cargo@v1
56 | with:
57 | command: clippy
58 | args: --all -- -D warnings
59 | - name: test
60 | if: contains(matrix.config.do, 'test')
61 | uses: actions-rs/cargo@v1
62 | with:
63 | command: test
64 | args: --verbose
65 |
--------------------------------------------------------------------------------
/.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 | /open-api*/
8 |
9 | # Added by cargo
10 |
11 | /target
12 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 | All notable changes to this project will be documented in this file.
3 |
4 | ## [unreleased]
5 |
6 | ### Documentation
7 |
8 | - Add proxy documentation
9 |
10 | ### Features
11 |
12 | - Add output file option to metadata
13 | - Add proxy support
14 | - Add more args to requests without impl
15 |
16 | ### Miscellaneous Tasks
17 |
18 | - Add lock
19 | - Only run ci if code or deps have changed
20 |
21 | ## [0.5.2.alpha] - 2022-01-15
22 |
23 | ### Bug Fixes
24 |
25 | - Use cargo action to install git cliff
26 | - Support multiline
27 | - Export changelog env
28 |
29 | ### Miscellaneous Tasks
30 |
31 | - Auto generate changelog
32 | - Fetch all tags
33 |
34 | ## [0.5.1.alpha] - 2022-01-15
35 |
36 | ### Miscellaneous Tasks
37 |
38 | - Strip binary
39 |
40 | ## [0.5.0.alpha] - 2022-01-15
41 |
42 | ### Bug Fixes
43 |
44 | - Use binary mode when no content type in response
45 |
46 | ### Features
47 |
48 | - Add separator and format json response
49 | - Allow to pipe output when downloading files
50 | - Add output file option to override default output
51 |
52 | ### Miscellaneous Tasks
53 |
54 | - Add postman echo examples
55 | - Bump version of many dependencies
56 | - Bump to version 0.5.0.alpha
57 |
58 | ### Refactor
59 |
60 | - Use option body
61 |
62 | ### Refeactor
63 |
64 | - Put args description in it's own file
65 |
66 | ## [0.4.3.alpha] - 2022-01-14
67 |
68 | ### Refactor
69 |
70 | - Declare trait for matching clap params
71 |
72 | ## [0.4.2.alpha] - 2022-01-14
73 |
74 | ### Bug Fixes
75 |
76 | - Use env var directly since on windows syntax is different
77 |
78 | ## [0.4.1.alpha] - 2022-01-14
79 |
80 | ### Bug Fixes
81 |
82 | - Remove duplicated env
83 |
84 | ## [0.4.0.alpha] - 2022-01-14
85 |
86 | ### Bug Fixes
87 |
88 | - Use env github token
89 |
90 | ### Features
91 |
92 | - Allow params to also be passed in command line
93 |
94 | ### Miscellaneous Tasks
95 |
96 | - Update workflow to use exec instead of post
97 | - Fix env
98 |
99 | ## [0.3.1] - 2022-01-14
100 |
101 | ### Bug Fixes
102 |
103 | - Use needs to share output betwwen jobs
104 | - Store share output betwwen jobs
105 | - Always upload artifact
106 |
107 | ### Miscellaneous Tasks
108 |
109 | - Add windows and macos builds
110 | - Fix build
111 |
112 | ## [0.3.1.alpha5] - 2022-01-13
113 |
114 | ### Bug Fixes
115 |
116 | - Add content-length for files
117 |
118 | ## [0.3.1.alpha4] - 2022-01-13
119 |
120 | ### Bug Fixes
121 |
122 | - Do not upload all build directory
123 |
124 | ## [0.3.1.alpha3] - 2022-01-13
125 |
126 | ### Bug Fixes
127 |
128 | - Use github template syntax
129 |
130 | ## [0.3.1.alpha2] - 2022-01-13
131 |
132 | ### Bug Fixes
133 |
134 | - Add readme.md not release.md
135 |
136 | ## [0.3.1.alpha1] - 2022-01-13
137 |
138 | ### Bug Fixes
139 |
140 | - Use options parameters
141 |
142 | ### Features
143 |
144 | - Auto detect pipe to disable coloring
145 |
146 | ### Miscellaneous Tasks
147 |
148 | - Add release upload
149 |
150 | ## [0.3.0-test] - 2022-01-12
151 |
152 | ### Bug Fixes
153 |
154 | - Clippy
155 | - Automatic fix
156 | - Fix clippy errors
157 |
158 | ### Documentation
159 |
160 | - Update readme
161 |
162 | ### Features
163 |
164 | - Impl basic get resource
165 | - Use clock emoji for spinners
166 |
167 | ### Miscellaneous Tasks
168 |
169 | - Improve pipeline
170 | - Fix pipeline
171 | - Add github create release example
172 | - Add create release workflow
173 |
174 | ## [0.3.0] - 2022-01-09
175 |
176 | ### Bug Fixes
177 |
178 | - Use match for set
179 | - Use anyhow everywhere
180 | - Better error display
181 | - Use new arg macro
182 | - Fix print bug when body result is empty
183 | - Upload bar orverride fix
184 | - Add progress component to main
185 |
186 | ### Documentation
187 |
188 | - Add some basic docs
189 | - Update readme
190 | - Update readme
191 |
192 | ### Features
193 |
194 | - Add basic config, theming and requests
195 | - Implement full configuration management
196 | - Improve output
197 | - Use default headers
198 | - Use singleton config
199 | - Improve manifests
200 | - Add support for tera templates
201 | - Add parameter input in terminal
202 | - Add support for process environnement variables
203 | - Context can use templates
204 | - Add support for default values from schema
205 | - Remove definitions
206 | - Set theme back to bat default
207 | - Implement chunk based file upload
208 | - Add upload progressbar
209 | - Add support for downloading files
210 | - Show percent file transfert
211 | - Replace static lazy with once cell
212 | - Add support for queries
213 | - Add dialog for creating requests
214 | - Add dynamic prompts for creating requests
215 | - Add a request editor
216 | - Allow to execute request by name
217 |
218 | ### Miscellaneous Tasks
219 |
220 | - Add disclaimer
221 | - Update clap
222 | - Update .gitignore
223 | - Disable imports
224 | - Add basic examples
225 | - Reorder deps
226 | - Add more examples
227 | - Add yaml example
228 | - Add ci
229 | - Add badges
230 | - Add logo
231 | - Add logo to readme
232 | - Update logo
233 | - Update readme
234 | - Improve readme
235 | - Bump version
236 |
237 | ### Refactor
238 |
239 | - Split cli into multiple files
240 | - Split request
241 | - Split config manifest
242 | - Use inline destructuring
243 | - Refactor execution
244 | - Split execute request
245 | - Rename and split request exec
246 | - Use filter to convert string to option
247 | - Split logic into upload/download component
248 | - Split progress bar in it's own file
249 |
250 | ### Styling
251 |
252 | - Convert to 2 space tabs
253 |
254 | ### Testing
255 |
256 | - Add tests for config
257 | - Add tests for languages
258 | - Use mocks for language tests
259 | - DRY tests
260 | - Use test_case library for improved DRY tests
261 | - Add tests
262 | - Add render tests
263 |
264 |
265 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | ecyrbe@gmail.com.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "apix"
3 | version = "0.6.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | anyhow = "1.0.52"
10 | atty = "0.2.14"
11 | bat = "0.18.3"
12 | clap = { version = "3.0.7", features=["regex", "cargo"] }
13 | clap_complete = "3.0.3"
14 | cmd_lib = "1.3.0"
15 | chrono = "0.4.19"
16 | comfy-table = "5.0.0"
17 | dialoguer = "0.9.0"
18 | dirs = "4.0.0"
19 | futures = "0.3.19"
20 | indexmap = { version = "1.8.0", features=["serde"]}
21 | indicatif = "0.16.2"
22 | jsonschema = "0.13.3"
23 | once_cell = "1.9.0"
24 | regex = "1.5.4"
25 | reqwest = { version="0.11.9", features = ["gzip","json","socks","stream"] }
26 | serde = "1.0.133"
27 | serde_json = { version = "1.0.74", features=["preserve_order"] }
28 | serde_yaml = "0.8.23"
29 | strum = "0.23.0"
30 | strum_macros = "0.23.1"
31 | term_size = "0.3.2"
32 | tokio = { version = "1.15.0", features = ["full"] }
33 | tokio-util = { version = "0.6.9", features = ["full"] }
34 | tera = "1.15.0"
35 | url = "2.2.2"
36 | whoami = "1.2.1"
37 |
38 | [dev-dependencies]
39 | test-case = "1.2.1"
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 ecyrbe
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | APIX is a modern HTTP client for the command line.
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | ```yaml
21 | 🚨 WARNING: 🚧 Apix is still in alpha/proof of concept state! 🚧
22 | ```
23 |
24 | Apix brings ideas from tools like `Git`,`Kubernetes`, `Helm` ,`Httpie`.
25 | Indeed it's is not just a simple HTTP client, Apix is :
26 | - **Pretty** as it uses [Bat](https://github.com/sharkdp/bat) to pretty print requests and responses
27 | ```bash
28 | > apix get https://apix.io/json
29 | {
30 | "id": 0,
31 | "test": "hello"
32 | }
33 | ```
34 | - **Beatifull** as it uses [Indicatif](https://docs.rs/indicatif/latest/indicatif/index.html) to show modern command line progress bars when uploading or downloading files
35 | ```bash
36 | > apix get https://apix.io/test.mp4
37 | Downloading File test.mp4
38 | 🕘 [00:00:28] [████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 14.98MiB/298.06MiB (549.87KiB/s, 8m)
39 | ```
40 | - **Friendly** to use as it uses [Dialoguer](https://docs.rs/dialoguer/latest/dialoguer/index.html) for interactive prompt to guide you when creating requests or executing them
41 | ```bash
42 | > apix exec -f request.yaml
43 | ✔ todoId · 1
44 | ? email ("ecyrbe@gmail.com") › bad\gmail.com
45 | ✘ Invalid input:
46 | cause 0: "bad\gmail.com" is not a "email"
47 | ```
48 | - **Powerfull** as it uses [Tera](https://tera.netlify.app/) template engine to allow your requests to do complex things if you want them to (see [examples](/examples))
49 | - **Easy** to use in the command line as it uses [Clap](https://docs.rs/clap/latest/clap/) autocompletion
50 | - **Reusable** as it stores your requests in your file system in a readable format for later use (yaml)
51 | - **Helping you not forget** as it stores the response of the requests so you can consult them at any time
52 | - **Helping you test APIs** as it allows you to create request stories (chain requests, use results of a request to make another one)
53 | - **Team player** as it allows you to naturally share your request collections with git
54 |
55 | Coming soon:
56 | - **Secure** as it handles secrets stored in hashi corp vault
57 | - **Enterprise friend** as it allows you to import OpenAPI definition files into apix requests files
58 |
59 | ## help
60 |
61 | ```bash
62 | apix 0.3.0
63 |
64 |
65 | USAGE:
66 | apix [OPTIONS]
67 |
68 | OPTIONS:
69 | -h, --help Print help information
70 | -v, --verbose print full request and response
71 | -V, --version Print version information
72 |
73 | SUBCOMMANDS:
74 | completions generate shell completions
75 | config configuration settings
76 | ctl apix control interface for handling multiple APIs
77 | delete delete an http resource
78 | exec execute a request from the current API context
79 | get get an http resource
80 | head get an http resource header
81 | help Print this message or the help of the given subcommand(s)
82 | history show history of requests sent (require project)
83 | init initialise a new API context in the current directory by using git
84 | patch patch an http resource
85 | post post to an http resource
86 | put put to an http resource
87 | ```
88 |
89 | ## make simple http requests
90 |
91 | Even if Apix allows you to use advanced mode by coupling it to a git repository and interpret openapi declarations (swagger), you also can use Apix as a replacement for curl, wget, httpie ...
92 |
93 | Apix will colorize the output according to http **content-type** header information.
94 |
95 | By default Apix will assume you are doing an API request using json.
96 |
97 | ```bash
98 | > apix get https://jsonplaceholder.typicode.com/todos?_limit=1
99 | or
100 | > apix get https://jsonplaceholder.typicode.com/todos --query _limit:1
101 |
102 | [
103 | {
104 | "userId": 1,
105 | "id": 1,
106 | "title": "delectus aut autem",
107 | "completed": false
108 | }
109 | ]
110 | ```
111 | you can also ask for verbose mode where apix will show you the full sended http request and response :
112 | ```bash
113 | > apix get -v https://jsonplaceholder.typicode.com/todos -q_limit:1
114 | GET /todos?_limit=3 HTTP/1.1
115 | host: jsonplaceholder.typicode.com
116 | user-agent: apix/0.1.0
117 | accept: application/json
118 | accept-encoding: gzip
119 | content-type: application/json
120 |
121 | HTTP/1.1 200 OK
122 | date: Sun, 21 Nov 2021 17:29:22 GMT
123 | content-type: application/json; charset=utf-8
124 | transfer-encoding: chunked
125 | connection: keep-alive
126 | access-control-allow-credentials: true
127 | cache-control: max-age=43200
128 | pragma: no-cache
129 | expires: -1
130 | etag: W/"136-fTr038fftlG9yIOWHGimupdrQDg"
131 |
132 | [
133 | {
134 | "userId": 1,
135 | "id": 1,
136 | "title": "delectus aut autem",
137 | "completed": false
138 | }
139 | ]
140 | ```
141 |
142 | ## Context
143 |
144 | Apix handle contexts gracefully. Contexts are named resources to handle:
145 | - API bindings (based on openAPI)
146 | - Session authentification
147 | - Session cookies
148 | - variables bindings that can be used with templates
149 |
150 | ### Create a new context
151 |
152 | ```bash
153 | apix ctl init MyContext
154 | ```
155 |
156 | ### Switching to another context
157 |
158 | ```bash
159 | apix ctl switch OtherContext
160 | ```
161 |
162 | ### List all contexts
163 |
164 | ```bash
165 | apix ctl get contexts
166 | ```
167 |
168 | ### Delete a context
169 |
170 | ```bash
171 | apix ctl delete MyContext
172 | ```
173 |
174 | ## apix commands
175 |
176 | ### apix get
177 |
178 | ```bash
179 | apix-get
180 |
181 | get an http resource
182 |
183 | USAGE:
184 | apix get [OPTIONS]
185 |
186 | ARGS:
187 | url to request, can be a 'Tera' template
188 |
189 | OPTIONS:
190 | -b, --body set body to send with request, can be a 'Tera' template
191 | -c, --cookie set cookie name:value to send with request
192 | -e, --env set variable name:value for 'Tera' template rendering
193 | -f, --file set body from file to send with request, can be a 'Tera' template
194 | -h, --help Print help information
195 | -H, --header set header name:value to send with request
196 | -i, --insecure allow insecure connections when using https
197 | -q, --query set query name:value to send with request
198 | -v, --verbose print full request and response
199 | ```
200 | ## Proxy
201 |
202 | Apix uses system proxy by default. System proxy is taken from `HTTP_PROXY` and `HTTPS_PROXY` environment variables.
203 | You can ovverride system proxy manually, either with request annotations on manifest files or on the command line.
204 | Apix support `HTTP`, `HTTPS` and `SOCKS5` proxies.
205 |
206 | Here are the options available for configuring the proxy on the command line :
207 | ```bash
208 | -x, --proxy set proxy to use for request
209 | --proxy-login set proxy login to use for request
210 | --proxy-password set proxy password to use for request
211 | ```
212 | Here are the options available for configuting the proxy on manifests
213 | ```yaml
214 | metadata:
215 | annotations:
216 | apix.io/proxy-url:
217 | apix.io/proxy-login:
218 | apix.io/proxy-password:
219 | ```
220 | _example URL for proxies_:
221 | | HTTP | HTTPS | SOCKS5 |
222 | | --------------------- | ---------------------- | ----------------------- |
223 | | http://localhost:3128 | https://localhost:3128 | socks5://localhost:1080 |
224 |
225 |
226 | # Persistance
227 |
228 | | type | persist mode | gitignore | description |
229 | | :------: | :----------: | :-------: | :-------------------------------------: |
230 | | config | file | no | from cli config |
231 | | requests | file | no |
232 | | params | file | yes | auto saved from dialoguer |
233 | | results | file | yes | auto saved after execution |
234 | | cookies | file | yes | auto saved after execution, auto reused |
235 | | storage | file | yes | saved from request response |
236 | | secrets | web | N/A | from hashi corp vault |
237 |
--------------------------------------------------------------------------------
/docs/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
116 |
--------------------------------------------------------------------------------
/examples/github-create-release.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: apix.io/v1
3 | metadata:
4 | name: github-create-release
5 | labels:
6 | app: apix
7 | apix.io/api: github
8 | annotations:
9 | apix.io/created-by: ecyrbe
10 | apix.io/created-at: "2022-01-12T20:00:09.830263999+00:00"
11 | kind: Request
12 | spec:
13 | request:
14 | method: POST
15 | url: "{{ env.GITHUB_API_URL }}/repos/{{ env.GITHUB_REPOSITORY }}/releases"
16 | headers:
17 | accept: application/vnd.github.v3+json
18 | authorization: Bearer {{ env.GITHUB_TOKEN }}
19 | body:
20 | tag_name: "{{ env.GITHUB_REF_NAME }}"
21 | target_commitish: "{{ env.GITHUB_SHA }}"
22 | name: "Release {{ env.GITHUB_REF_NAME }}"
23 | body: "{{ env.CHANGELOG }}"
24 | draft: false
25 | prerelease: false
26 | generate_release_notes: true
27 |
--------------------------------------------------------------------------------
/examples/github-upload-release-asset.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: apix.io/v1
3 | metadata:
4 | name: github-upload-release-asset
5 | labels:
6 | app: apix
7 | apix.io/api: github
8 | annotations:
9 | apix.io/created-by: ecyrbe
10 | apix.io/created-at: "2022-01-12T20:00:09.830263999+00:00"
11 | apix.io/body-file: "../target/release/{{ env.ASSET }}"
12 | kind: Request
13 | spec:
14 | request:
15 | method: POST
16 | url: https://uploads.github.com/repos/{{ env.GITHUB_REPOSITORY }}/releases/{{ env.RELEASE_ID }}/assets
17 | headers:
18 | accept: application/vnd.github.v3+json
19 | content-type: application/gzip
20 | authorization: Bearer {{ env.GITHUB_TOKEN }}
21 | queries:
22 | name: "{{ env.ASSET }}"
23 |
--------------------------------------------------------------------------------
/examples/postman-echo-delete.yaml:
--------------------------------------------------------------------------------
1 | # example to execute this request:
2 | # apix exec postman-echo-post
3 | apiVersion: apix.io/v1
4 | metadata:
5 | name: postman-echo-delete
6 | kind: Request
7 | spec:
8 | request:
9 | method: delete
10 | url: https://postman-echo.com/delete
11 | headers:
12 | accept: application/json
13 | queries:
14 | param1: value1
15 | param2: value2
16 |
--------------------------------------------------------------------------------
/examples/postman-echo-get.yaml:
--------------------------------------------------------------------------------
1 | # example to execute this request:
2 | # apix exec postman-echo-get
3 | apiVersion: apix.io/v1
4 | metadata:
5 | name: postman-echo-get
6 | kind: Request
7 | spec:
8 | request:
9 | method: get
10 | url: https://postman-echo.com/get
11 | headers:
12 | accept: application/json
13 | queries:
14 | param1: value1
15 | param2: value2
16 |
--------------------------------------------------------------------------------
/examples/postman-echo-patch.yaml:
--------------------------------------------------------------------------------
1 | # example to execute this request:
2 | # apix exec postman-echo-patch
3 | apiVersion: apix.io/v1
4 | metadata:
5 | name: postman-echo-patch
6 | kind: Request
7 | spec:
8 | request:
9 | method: patch
10 | url: https://postman-echo.com/patch
11 | headers:
12 | accept: application/json
13 | queries:
14 | param1: value1
15 | param2: value2
16 | body:
17 | body1: value1
18 | body2: value2
19 | body3:
20 | - item1
21 | - item2
22 |
--------------------------------------------------------------------------------
/examples/postman-echo-post-file.yaml:
--------------------------------------------------------------------------------
1 | # example to execute this request:
2 | # apix exec postman-echo-post-file
3 | apiVersion: apix.io/v1
4 | metadata:
5 | name: postman-echo-post-file
6 | annotations:
7 | apix.io/body-file: ../docs/logo.svg
8 | kind: Request
9 | spec:
10 | request:
11 | method: post
12 | url: https://postman-echo.com/post
13 | headers:
14 | accept: application/json
15 | content-type: image/svg+xml
16 | queries:
17 | param1: value1
18 | param2: value2
19 |
--------------------------------------------------------------------------------
/examples/postman-echo-post.yaml:
--------------------------------------------------------------------------------
1 | # example to execute this request:
2 | # apix exec postman-echo-post
3 | apiVersion: apix.io/v1
4 | metadata:
5 | name: postman-echo-post
6 | kind: Request
7 | spec:
8 | request:
9 | method: post
10 | url: https://postman-echo.com/post
11 | headers:
12 | accept: application/json
13 | queries:
14 | param1: value1
15 | param2: value2
16 | body:
17 | body1: value1
18 | body2: value2
19 | body3:
20 | - item1
21 | - item2
22 |
--------------------------------------------------------------------------------
/examples/postman-echo-put.yaml:
--------------------------------------------------------------------------------
1 | # example to execute this request:
2 | # apix exec postman-echo-post
3 | apiVersion: apix.io/v1
4 | metadata:
5 | name: postman-echo-put
6 | kind: Request
7 | spec:
8 | request:
9 | method: put
10 | url: https://postman-echo.com/put
11 | headers:
12 | accept: application/json
13 | queries:
14 | param1: value1
15 | param2: value2
16 | body:
17 | body1: value1
18 | body2: value2
19 | body3:
20 | - item1
21 | - item2
22 |
--------------------------------------------------------------------------------
/examples/postman-echo-stream.yaml:
--------------------------------------------------------------------------------
1 | # example to execute this request:
2 | # apix exec postman-echo-stream
3 | apiVersion: apix.io/v1
4 | metadata:
5 | name: postman-echo-stream
6 | annotations:
7 | apix.io/created-by: ecyrbe
8 | apix.io/created-at: "2022-01-12T20:00:09.830263999+00:00"
9 | apix.io/output-file: stream.json
10 | kind: Request
11 | spec:
12 | request:
13 | method: get
14 | url: https://postman-echo.com/stream/100
15 | headers:
16 | accept: application/json
17 | queries:
18 | param1: value1
19 | param2: value2
20 |
--------------------------------------------------------------------------------
/examples/request.yaml:
--------------------------------------------------------------------------------
1 | # example to execute this request:
2 | # apix exec request
3 | apiVersion: apix.io/v1
4 | metadata:
5 | name: request
6 | annotations:
7 | apix.io/convert-body-string-to-json: "true"
8 | kind: Request
9 | spec:
10 | parameters:
11 | - name: todo
12 | required: true
13 | schema:
14 | type: integer
15 | default: 1
16 | - name: email
17 | required: true
18 | schema:
19 | type: string
20 | format: email
21 | default: ecyrbe@gmail.com
22 | - name: content
23 | required: true
24 | schema:
25 | type: object
26 | default:
27 | title: "My todo"
28 | description: "My todo description"
29 | done: false
30 | context:
31 | # you can create new variables in the context
32 | url: https://jsonplaceholder.typicode.com/todos
33 | # you can use variables from parameters in the context
34 | next: "{{ parameters.todo + 1 }}"
35 | # you can use variables from introspection in the context
36 | annotation: '{{ manifest.metadata.annotations["apix.io/convert-body-string-to-json"] }}'
37 | # you cannot use variables created in the context
38 | # error: "{{ next + 1 }}" # variable next is not defined in context itself
39 | request:
40 | method: post
41 | # you can use variables from the context
42 | url: "{{ context.url }}"
43 | headers:
44 | accept: application/json
45 | # you can use variables from the process environnement
46 | # you can use variables from the context
47 | # you can use variables from the parameters
48 | # you can use variables from the introspection
49 | # you can use variables from the tera only in scoped content (ie: variable declared in url can't be used in body)
50 | # you can use filters to transform the variable
51 | body: |-
52 | {% set my_var = "test" %}
53 | {
54 | "user": "{{ env.USERNAME }}",
55 | "todo": {{ parameters.todo }},
56 | "next": {{ context.next }},
57 | "hello": "{{ parameters.email }}",
58 | "annotation": "{{ context.annotation }}",
59 | "content": {{ parameters.content | json_encode }},
60 | "test": "{{ my_var }}"
61 | }
62 |
--------------------------------------------------------------------------------
/examples/request_html.yaml:
--------------------------------------------------------------------------------
1 | # example to execute this request:
2 | # apix exec request_html
3 | apiVersion: apix.io/v1
4 | metadata:
5 | name: request_html
6 | kind: Request
7 | spec:
8 | request:
9 | method: get
10 | url: https://google.com
11 | headers:
12 | accept: text/html
13 |
--------------------------------------------------------------------------------
/examples/request_with_advanced_template.yaml:
--------------------------------------------------------------------------------
1 | # example to execute this request:
2 | # apix exec request_with_advanced_template
3 | apiVersion: apix.io/v1
4 | metadata:
5 | name: request_with_advanced_template
6 | annotations:
7 | apix.io/convert-body-string-to-json: "true"
8 | kind: Request
9 | spec:
10 | context:
11 | # you can create new variables in the context
12 | url: https://jsonplaceholder.typicode.com/users
13 | info:
14 | email: ecyrbe@gmail.com
15 | name: ecyrbe
16 | birthday: "01/01/1989"
17 | request:
18 | method: post
19 | # you can use variables from the context and parameters
20 | url: "{{ context.url }}"
21 | headers:
22 | accept: application/json
23 | content-type: application/json
24 | # you can loop over anything in the context, env, parameters, introspection
25 | # you can use if conditions to filter the loop
26 | body: |-
27 | {
28 | {% for name,value in context.info -%}
29 | {% if name != 'birthday' -%}
30 | "{{ name }}": "{{ value }}",
31 | {%- endif %}
32 | {%- endfor %}
33 | "name": "{{ env.USERNAME }}"
34 | }
35 |
--------------------------------------------------------------------------------
/examples/request_with_env.yaml:
--------------------------------------------------------------------------------
1 | # example to execute this request:
2 | # apix exec request_with_env
3 | apiVersion: apix.io/v1
4 | metadata:
5 | name: request_with_env
6 | kind: Request
7 | spec:
8 | context:
9 | # you can create new variables in the context
10 | url: https://jsonplaceholder.typicode.com/users
11 | user: USERNAME
12 | request:
13 | method: post
14 | # you can use variables from the context and parameters
15 | url: "{{ context.url }}"
16 | headers:
17 | accept: application/json
18 | content-type: application/json
19 | # you can use variables from the process environnement with simplified syntax
20 | # you can use variables from the process environnement with tera get_env function
21 | body:
22 | name: "{{ env.USERNAME }}"
23 | dynamicName: "{{ env[context.user] }}"
24 | functionName: "{{ get_env(name='USERNAME') }}"
25 | dynamicFunctionName: "{{ get_env(name=context.user) }}"
26 |
--------------------------------------------------------------------------------
/examples/request_with_file.yaml:
--------------------------------------------------------------------------------
1 | # example to execute this request:
2 | # apix exec request_with_file
3 | apiVersion: apix.io/v1
4 | metadata:
5 | name: request_with_file
6 | annotations:
7 | # you can upload files to body instead of using the body field
8 | # but templating is not supported for files
9 | # you should then use external files to upload large binary files
10 | apix.io/body-file: "test.json"
11 | kind: Request
12 | spec:
13 | request:
14 | method: post
15 | url: https://jsonplaceholder.typicode.com/users
16 | headers:
17 | accept: application/json
18 | content-type: application/json
19 |
--------------------------------------------------------------------------------
/examples/request_with_params.yaml:
--------------------------------------------------------------------------------
1 | # example to execute this request:
2 | # apix exec request_with_params
3 | apiVersion: apix.io/v1
4 | metadata:
5 | name: request_with_params
6 | kind: Request
7 | spec:
8 | parameters:
9 | - name: todoId
10 | required: true
11 | schema:
12 | type: integer
13 | default: 1
14 | minimum: 1
15 | maximum: 200
16 | context:
17 | # you can create new variables in the context
18 | url: https://jsonplaceholder.typicode.com/todos
19 | request:
20 | method: get
21 | # you can use variables from the context and parameters
22 | url: "{{ context.url }}/{{ parameters.todoId }}"
23 | headers:
24 | accept: application/json
25 |
--------------------------------------------------------------------------------
/examples/request_with_query_params.yaml:
--------------------------------------------------------------------------------
1 | # example to execute this request:
2 | # apix exec request_with_query_params
3 | apiVersion: apix.io/v1
4 | metadata:
5 | name: request_with_query_params
6 | kind: Request
7 | spec:
8 | parameters:
9 | - name: max_results
10 | required: true
11 | schema:
12 | type: integer
13 | default: 1
14 | minimum: 1
15 | maximum: 200
16 | context:
17 | # you can create new variables in the context
18 | url: https://jsonplaceholder.typicode.com/todos
19 | request:
20 | method: get
21 | # you can use variables from the context and parameters
22 | url: "{{ context.url }}"
23 | headers:
24 | accept: application/json
25 | queries:
26 | _limit: "{{ parameters.max_results }}"
27 |
--------------------------------------------------------------------------------
/examples/request_yaml.yaml:
--------------------------------------------------------------------------------
1 | # example to execute this request:
2 | # apix exec request_yaml
3 | apiVersion: apix.io/v1
4 | metadata:
5 | name: request_yaml
6 | kind: Request
7 | spec:
8 | request:
9 | method: get
10 | url: https://raw.githubusercontent.com/ecyrbe/apix-rust/main/examples/request_yaml.yaml
11 | headers:
12 | accept: text/x-yaml
13 |
--------------------------------------------------------------------------------
/rustfmt.toml:
--------------------------------------------------------------------------------
1 | tab_spaces = 2
2 | max_width = 120
3 |
--------------------------------------------------------------------------------
/src/build_args.rs:
--------------------------------------------------------------------------------
1 | use super::match_params::RequestParam;
2 | use super::validators::{validate_param, validate_url};
3 | use clap::{crate_authors, crate_version, App, AppSettings, Arg, ValueHint};
4 | use clap_complete::Shell;
5 | use once_cell::sync::Lazy;
6 |
7 | pub fn build_request_args() -> impl Iterator- > {
8 | static ARGS: Lazy<[Arg<'static>; 17]> = Lazy::new(|| {
9 | [
10 | Arg::new("url")
11 | .help("url to request, can be a 'Tera' template")
12 | .required(true)
13 | .value_hint(ValueHint::Url)
14 | .validator(validate_url),
15 | Arg::new("header")
16 | .short('H')
17 | .long("header")
18 | .help("set header name:value to send with request")
19 | .multiple_occurrences(true)
20 | .takes_value(true)
21 | .validator(|param| validate_param(param, RequestParam::Header)),
22 | Arg::new("cookie")
23 | .short('c')
24 | .long("cookie")
25 | .help("set cookie name:value to send with request")
26 | .multiple_occurrences(true)
27 | .takes_value(true)
28 | .validator(|param| validate_param(param, RequestParam::Cookie)),
29 | Arg::new("query")
30 | .short('q')
31 | .long("query")
32 | .help("set query name:value to send with request")
33 | .multiple_occurrences(true)
34 | .takes_value(true)
35 | .validator(|param| validate_param(param, RequestParam::Query)),
36 | Arg::new("body")
37 | .short('b')
38 | .long("body")
39 | .help("set body to send with request, can be a 'Tera' template")
40 | .takes_value(true)
41 | .conflicts_with("file"),
42 | Arg::new("file")
43 | .short('f')
44 | .long("file")
45 | .help("set body from file to send with request, can be a 'Tera' template")
46 | .takes_value(true)
47 | .conflicts_with("body")
48 | .value_hint(ValueHint::FilePath),
49 | Arg::new("param")
50 | .short('p')
51 | .long("param")
52 | .help("set parameter name:value for 'Tera' template rendering")
53 | .multiple_occurrences(true)
54 | .takes_value(true)
55 | .validator(|param| validate_param(param, RequestParam::Param)),
56 | Arg::new("proxy")
57 | .help("set proxy url to use for request")
58 | .short('x')
59 | .long("proxy")
60 | .value_hint(ValueHint::Url)
61 | .takes_value(true),
62 | Arg::new("proxy-login")
63 | .help("set proxy login to use for request")
64 | .long("proxy-login")
65 | .takes_value(true),
66 | Arg::new("proxy-password")
67 | .help("set proxy password to use for request")
68 | .long("proxy-password")
69 | .takes_value(true),
70 | Arg::new("follow")
71 | .help("follow http redirects")
72 | .short('F')
73 | .long("follow"),
74 | Arg::new("max-redirects")
75 | .help("set max http redirects to follow")
76 | .long("max-redirects")
77 | .takes_value(true),
78 | Arg::new("timeout")
79 | .help("set request timeout in seconds")
80 | .long("timeout")
81 | .takes_value(true),
82 | Arg::new("user-agent")
83 | .help("set user agent to send with request")
84 | .long("user-agent")
85 | .takes_value(true),
86 | Arg::new("certificate")
87 | .help("add a custom certificate authority")
88 | .long("certificate")
89 | .takes_value(true)
90 | .multiple_occurrences(true)
91 | .value_hint(ValueHint::FilePath),
92 | Arg::new("bind-address")
93 | .help("set bind address for request")
94 | .long("bind-address")
95 | .takes_value(true),
96 | Arg::new("insecure")
97 | .help("allow insecure connections when using https")
98 | .long("insecure"),
99 | ]
100 | });
101 | ARGS.iter()
102 | }
103 |
104 | pub fn build_exec_args() -> impl Iterator
- > {
105 | static EXEC_ARGS: Lazy<[Arg<'static>; 6]> = Lazy::new(|| {
106 | [
107 | Arg::new("name").help("name of the request to execute").index(1),
108 | Arg::new("file")
109 | .help("Execute a manifest file request directly")
110 | .short('f')
111 | .long("file")
112 | .takes_value(true)
113 | .value_hint(ValueHint::FilePath)
114 | .conflicts_with("name"),
115 | Arg::new("param")
116 | .help("Set a parameter for the request")
117 | .short('p')
118 | .long("param")
119 | .multiple_occurrences(true)
120 | .takes_value(true)
121 | .validator(|param| validate_param(param, RequestParam::Param)),
122 | Arg::new("proxy")
123 | .help("set proxy to use for request")
124 | .short('x')
125 | .long("proxy")
126 | .takes_value(true),
127 | Arg::new("proxy-login")
128 | .help("set proxy login to use for request")
129 | .long("proxy-login")
130 | .takes_value(true),
131 | Arg::new("proxy-password")
132 | .help("set proxy password to use for request")
133 | .long("proxy-password")
134 | .takes_value(true),
135 | ]
136 | });
137 | EXEC_ARGS.iter()
138 | }
139 |
140 | pub fn build_create_request_args() -> impl Iterator
- > {
141 | static CREATE_ARGS: Lazy<[Arg<'static>; 10]> = Lazy::new(|| {
142 | [
143 | Arg::new("name").help("name of request to create").index(1),
144 | Arg::new("method")
145 | .help("method of request to create")
146 | .possible_values(["GET", "POST", "PUT", "DELETE"])
147 | .ignore_case(true)
148 | .index(2),
149 | Arg::new("url")
150 | .help("url to request, can be a 'Tera' template")
151 | .validator(validate_url)
152 | .index(3),
153 | Arg::new("header")
154 | .short('H')
155 | .long("header")
156 | .help("set header name:value to send with request")
157 | .multiple_occurrences(true)
158 | .takes_value(true)
159 | .validator(|param| validate_param(param, RequestParam::Header)),
160 | Arg::new("cookie")
161 | .short('c')
162 | .long("cookie")
163 | .help("set cookie name:value to send with request")
164 | .multiple_occurrences(true)
165 | .takes_value(true)
166 | .validator(|param| validate_param(param, RequestParam::Cookie)),
167 | Arg::new("query")
168 | .short('q')
169 | .long("query")
170 | .help("set query name:value to send with request")
171 | .multiple_occurrences(true)
172 | .takes_value(true)
173 | .validator(|param| validate_param(param, RequestParam::Query)),
174 | Arg::new("body")
175 | .short('b')
176 | .long("body")
177 | .help("set body to send with request, can be a 'Tera' template")
178 | .takes_value(true)
179 | .conflicts_with("file"),
180 | Arg::new("file")
181 | .short('f')
182 | .long("file")
183 | .help("set body from file to send with request, can be a 'Tera' template")
184 | .takes_value(true)
185 | .conflicts_with("body")
186 | .value_hint(ValueHint::FilePath),
187 | Arg::new("param")
188 | .short('p')
189 | .long("param")
190 | .help("set parameter name:value for 'Tera' template rendering")
191 | .multiple_occurrences(true)
192 | .takes_value(true)
193 | .validator(|param| validate_param(param, RequestParam::Param)),
194 | Arg::new("insecure")
195 | .help("allow insecure connections when using https")
196 | .short('i')
197 | .long("insecure"),
198 | ]
199 | });
200 | CREATE_ARGS.iter()
201 | }
202 |
203 | pub fn build_cli() -> App<'static> {
204 | App::new("apix")
205 | .setting(AppSettings::SubcommandRequiredElseHelp)
206 | .version(crate_version!())
207 | .author(crate_authors!())
208 | .args([
209 | Arg::new("verbose")
210 | .help("print full request and response")
211 | .short('v')
212 | .long("verbose")
213 | .global(true),
214 | Arg::new("output-file")
215 | .help("output file")
216 | .short('o')
217 | .long("output-file")
218 | .takes_value(true)
219 | .value_hint(ValueHint::FilePath)
220 | .global(true),
221 | ])
222 | .subcommands([
223 | App::new("completions").about("generate shell completions").arg(
224 | Arg::new("shell")
225 | .help("shell to target for completions")
226 | .possible_values(Shell::possible_values())
227 | .required(true),
228 | ),
229 | App::new("config")
230 | .setting(AppSettings::SubcommandRequiredElseHelp)
231 | .about("configuration settings")
232 | .subcommands([
233 | App::new("list"),
234 | App::new("set").about("set configuration value").args([
235 | Arg::new("name")
236 | .help("name of configuration value to set")
237 | .required(true)
238 | .index(1),
239 | Arg::new("value")
240 | .help("value to set configuration value to")
241 | .required(true)
242 | .index(2),
243 | ]),
244 | App::new("get").about("get a configuration value").arg(
245 | Arg::new("name")
246 | .help("name of configuration value to get")
247 | .required(true),
248 | ),
249 | App::new("delete").about("delete a configuration value").arg(
250 | Arg::new("name")
251 | .help("name of configuration value to delete")
252 | .required(true),
253 | ),
254 | ]),
255 | App::new("init").about("initialise a new API context in the current directory by using git"),
256 | App::new("history").about("show history of requests sent (require project)"),
257 | App::new("get").about("get an http resource").args(build_request_args()),
258 | App::new("head")
259 | .about("get an http resource header")
260 | .args(build_request_args()),
261 | App::new("post")
262 | .about("post to an http resource")
263 | .args(build_request_args()),
264 | App::new("delete")
265 | .about("delete an http resource")
266 | .args(build_request_args()),
267 | App::new("put")
268 | .about("put to an http resource")
269 | .args(build_request_args()),
270 | App::new("patch")
271 | .about("patch an http resource")
272 | .args(build_request_args()),
273 | App::new("exec")
274 | .about("execute a request from the current API context")
275 | .args(build_exec_args()),
276 | App::new("ctl")
277 | .setting(AppSettings::SubcommandRequiredElseHelp)
278 | .about("apix control interface for handling multiple APIs")
279 | .subcommands([
280 | App::new("switch").about("switch API context"),
281 | App::new("apply").about("apply an apix manifest into current project"),
282 | App::new("create")
283 | .setting(AppSettings::SubcommandRequiredElseHelp)
284 | .about("create a new apix manifest")
285 | .subcommands([
286 | App::new("request")
287 | .about("create a new request")
288 | .args(build_create_request_args()),
289 | App::new("story").about("create a new story"),
290 | // .args(build_create_story_args()),
291 | ]),
292 | App::new("edit")
293 | .about("edit an existing apix resource with current terminal EDITOR")
294 | .args([
295 | Arg::new("resource")
296 | .help("resource type to edit")
297 | .possible_values(["resource", "context", "story", "request", "config"])
298 | .index(1),
299 | Arg::new("name").help("name of apix resource to edit").index(2),
300 | Arg::new("file")
301 | .help("Edit a resource file directly")
302 | .short('f')
303 | .long("file")
304 | .takes_value(true)
305 | .value_hint(ValueHint::FilePath)
306 | .conflicts_with_all(&["resource", "name"]),
307 | ]),
308 | App::new("get").about("get information about an apix resource").args([
309 | Arg::new("resource")
310 | .possible_values(["resource", "context", "story", "request"])
311 | .index(1),
312 | Arg::new("name").help("name of apix resource to edit").index(2),
313 | ]),
314 | App::new("delete").about("delete an existing named resource").args([
315 | Arg::new("resource")
316 | .help("resource type to delete")
317 | .possible_values(["resource", "context", "story", "request"])
318 | .required(true)
319 | .index(1),
320 | Arg::new("name")
321 | .help("name of apix resource to delete")
322 | .required(true)
323 | .index(2),
324 | ]),
325 | App::new("import")
326 | .about("import an OpenAPI description file in yaml or json")
327 | .arg(
328 | Arg::new("url")
329 | .help("Filename or URL to openApi description to import")
330 | .required(true),
331 | ),
332 | ]),
333 | ])
334 | }
335 |
--------------------------------------------------------------------------------
/src/dialog.rs:
--------------------------------------------------------------------------------
1 | use super::manifests::ApixParameter;
2 | use anyhow::Result;
3 | use dialoguer::{theme::ColorfulTheme, Input, Password};
4 | use jsonschema::{Draft, JSONSchema};
5 | use serde_json::Value;
6 |
7 | fn input_to_value(input: &str) -> Value {
8 | match serde_json::from_str(input) {
9 | Ok(value) => value,
10 | _ => Value::String(input.to_string()),
11 | }
12 | }
13 |
14 | pub trait Dialog {
15 | fn ask(&self) -> Result;
16 | }
17 |
18 | impl Dialog for ApixParameter {
19 | fn ask(&self) -> Result {
20 | let value_schema = self.schema.as_ref().unwrap();
21 | let schema = JSONSchema::options()
22 | .with_draft(Draft::Draft7)
23 | .compile(value_schema)
24 | .map_err(|err| anyhow::anyhow!("{}", err))?;
25 | if self.password {
26 | let input = Password::with_theme(&ColorfulTheme::default())
27 | .with_prompt(&self.name)
28 | .interact()?;
29 |
30 | Ok(Value::String(input))
31 | } else {
32 | // check if schema has a default value
33 | let default = value_schema.as_object().and_then(|obj| obj.get("default"));
34 | let theme = ColorfulTheme::default();
35 | let mut input = Input::with_theme(&theme);
36 | input.with_prompt(&self.name);
37 | if let Some(default) = default {
38 | input.default(serde_json::to_string(default)?);
39 | }
40 | let value = input
41 | .validate_with(|input: &String| {
42 | let value = input_to_value(input);
43 | let result = schema.validate(&value);
44 | if let Err(errors) = result {
45 | let mut msg: Vec = vec!["Invalid input:".to_string()];
46 | for (index, cause) in errors.enumerate() {
47 | msg.push(format!("cause {}: {}", index, cause));
48 | }
49 | return Err(msg.join("\n"));
50 | }
51 | Ok(())
52 | })
53 | .interact_text()?;
54 |
55 | Ok(input_to_value(&value))
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/display.rs:
--------------------------------------------------------------------------------
1 | use std::path::PathBuf;
2 |
3 | use super::http_utils::Language;
4 | use anyhow::Result;
5 | use bat::{Input, PrettyPrinter};
6 | use reqwest::{Request, Response};
7 | use serde_json::Value;
8 | use term_size::dimensions_stdout;
9 | use url::Position;
10 |
11 | pub trait HttpDisplay {
12 | fn print(&self, theme: &str, enable_color: bool) -> Result<()>;
13 | }
14 |
15 | pub fn print_separator() {
16 | if let Some((width, _)) = dimensions_stdout() {
17 | println!("{}", "─".repeat(width));
18 | }
19 | }
20 |
21 | pub fn pretty_print(content: String, theme: &str, language: &str, enable_color: bool) -> Result<()> {
22 | match language {
23 | "json" => {
24 | let json: Value = serde_json::from_str(&content)?;
25 | let formatted = serde_json::to_string_pretty(&json)?;
26 | PrettyPrinter::new()
27 | .input(Input::from_reader(formatted.as_bytes()))
28 | .language(language)
29 | .colored_output(enable_color)
30 | .theme(theme)
31 | .print()
32 | .map_err(|err| anyhow::anyhow!("Failed to print result: {:#}", err))?;
33 | }
34 | _ => {
35 | PrettyPrinter::new()
36 | .input(Input::from_reader(content.as_bytes()))
37 | .language(language)
38 | .colored_output(enable_color)
39 | .theme(theme)
40 | .print()
41 | .map_err(|err| anyhow::anyhow!("Failed to print result: {:#}", err))?;
42 | }
43 | }
44 | Ok(())
45 | }
46 |
47 | pub fn pretty_print_file(path: PathBuf, theme: &str, language: &str, enable_color: bool) -> Result<()> {
48 | PrettyPrinter::new()
49 | .input_file(path)
50 | .language(language)
51 | .colored_output(enable_color)
52 | .theme(theme)
53 | .grid(true)
54 | .header(true)
55 | .line_numbers(true)
56 | .print()
57 | .map_err(|err| anyhow::anyhow!("Failed to print result: {:#}", err))?;
58 | Ok(())
59 | }
60 |
61 | impl HttpDisplay for Request {
62 | fn print(&self, theme: &str, enable_color: bool) -> Result<()> {
63 | let mut output = format!(
64 | "{method} {endpoint} {protocol:?}\nhost: {host}\n",
65 | method = self.method(),
66 | endpoint = &self.url()[Position::BeforePath..],
67 | protocol = self.version(),
68 | host = self
69 | .url()
70 | .host_str()
71 | .ok_or_else(|| anyhow::anyhow!("invalid host in URL: {}", self.url()))?
72 | );
73 | for (key, value) in self.headers() {
74 | output.push_str(&format!("{}: {}\n", key.as_str(), value.to_str()?));
75 | }
76 | pretty_print(output, theme, "yaml", enable_color)?;
77 |
78 | // pretty print body if present and it has a content type that match a language
79 | if let (Some(body), Some(language)) = (self.body(), self.get_language()) {
80 | println!();
81 | if let Some(bytes) = body.as_bytes() {
82 | PrettyPrinter::new()
83 | .input(Input::from_reader(bytes))
84 | .language(language)
85 | .colored_output(enable_color)
86 | .theme(theme)
87 | .print()
88 | .map_err(|err| anyhow::anyhow!("Failed to print result: {:#}", err))?;
89 | }
90 | }
91 | Ok(())
92 | }
93 | }
94 |
95 | impl HttpDisplay for Response {
96 | fn print(&self, theme: &str, enable_color: bool) -> Result<()> {
97 | let mut output = format!(
98 | "{protocol:?} {status}\n",
99 | protocol = self.version(),
100 | status = self.status()
101 | );
102 | for (key, value) in self.headers() {
103 | output.push_str(&format!("{}: {}\n", key.as_str(), value.to_str()?));
104 | }
105 | pretty_print(output, theme, "yaml", enable_color)?;
106 | Ok(())
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/editor.rs:
--------------------------------------------------------------------------------
1 | use anyhow::Result;
2 | use std::ffi::OsString;
3 |
4 | // get the user default editor
5 | fn get_default_editor() -> OsString {
6 | if let Some(prog) = std::env::var_os("VISUAL") {
7 | return prog;
8 | }
9 | if let Some(prog) = std::env::var_os("EDITOR") {
10 | return prog;
11 | }
12 | if cfg!(windows) {
13 | "notepad.exe".into()
14 | } else {
15 | "vi".into()
16 | }
17 | }
18 |
19 | // edit file with default editor
20 | pub fn edit_file(file: &str) -> Result<()> {
21 | let editor = get_default_editor();
22 | std::process::Command::new(&editor).arg(file).spawn()?.wait()?;
23 | Ok(())
24 | }
25 |
--------------------------------------------------------------------------------
/src/execute.rs:
--------------------------------------------------------------------------------
1 | use crate::manifests::ApixRequest;
2 | use crate::requests::{make_request, AdvancedBody, RequestOptions};
3 |
4 | use super::dialog::Dialog;
5 | use super::template::{MapTemplate, StringTemplate, ValueTemplate};
6 | use super::{ApixKind, ApixManifest};
7 | use anyhow::Result;
8 | use indexmap::IndexMap;
9 | use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
10 | use serde_json::Value;
11 | use std::collections::HashMap;
12 | use std::str::FromStr;
13 | use tera::{Context, Tera};
14 |
15 | struct RequestTemplate<'a> {
16 | request: &'a ApixRequest,
17 | engine: Tera,
18 | context: Context,
19 | file: &'a str,
20 | annotations: IndexMap,
21 | }
22 |
23 | #[derive(Debug, Clone)]
24 | struct RequestParams<'a> {
25 | url: String,
26 | method: String,
27 | headers: HeaderMap,
28 | queries: IndexMap,
29 | body: Option,
30 | options: RequestOptions<'a>,
31 | }
32 |
33 | // ask for all parameters in manifest request
34 | fn ask_for_required_parameters(
35 | request: &ApixRequest,
36 | params: &Option>,
37 | ) -> Result, anyhow::Error> {
38 | match params {
39 | Some(params) => request
40 | .parameters
41 | .iter()
42 | .filter(|param| param.required || params.get(¶m.name).is_some())
43 | .map(|parameter| {
44 | if let Some(param) = params.get(¶meter.name) {
45 | Ok((parameter.name.clone(), Value::String(param.clone())))
46 | } else {
47 | Ok((parameter.name.clone(), parameter.ask()?))
48 | }
49 | })
50 | .collect(),
51 | None => request
52 | .parameters
53 | .iter()
54 | .filter(|param| param.required)
55 | .map(|parameter| Ok((parameter.name.clone(), parameter.ask()?)))
56 | .collect(),
57 | }
58 | }
59 |
60 | impl<'a> RequestTemplate<'a> {
61 | fn new(manifest: &'a ApixManifest, file: &'a str, params: &Option>) -> Result {
62 | match manifest.kind() {
63 | ApixKind::Request(request) => {
64 | let parameters = Value::Object(ask_for_required_parameters(request, params)?);
65 | let env: HashMap = std::env::vars().collect();
66 | let mut engine = Tera::default();
67 | let mut context = Context::new();
68 |
69 | context.insert("manifest", &manifest);
70 | context.insert("parameters", ¶meters);
71 | context.insert("env", &env);
72 |
73 | let annotations = engine.render_map(
74 | &format!("{}#/annotations", file),
75 | manifest.get_annotations().unwrap_or(&IndexMap::::new()),
76 | &context,
77 | )?;
78 |
79 | Ok(Self {
80 | request,
81 | engine,
82 | context,
83 | file,
84 | annotations,
85 | })
86 | }
87 | _ => Err(anyhow::anyhow!("Request manifest expected")),
88 | }
89 | }
90 |
91 | fn render_context(&mut self) -> Result<&mut Self> {
92 | let rendered_context = self.engine.render_value(
93 | &format!("{}#/context", self.file),
94 | &Value::Object(serde_json::Map::from_iter(self.request.context.clone().into_iter())),
95 | &self.context,
96 | )?;
97 | self.context.insert("context", &rendered_context);
98 | Ok(self)
99 | }
100 |
101 | fn render_options(&mut self, options: &RequestOptions<'a>) -> RequestOptions<'a> {
102 | let output_filename = self.annotations.get("apix.io/output-file").map(String::to_owned);
103 | let proxy_url = self.annotations.get("apix.io/proxy-url").map(String::to_owned);
104 | let proxy_login = self.annotations.get("apix.io/proxy-login").map(String::to_owned);
105 | let proxy_password = self.annotations.get("apix.io/proxy-password").map(String::to_owned);
106 | let options = options.clone();
107 | RequestOptions {
108 | output_filename: options.output_filename.or(output_filename),
109 | proxy_url: options.proxy_url.or(proxy_url),
110 | proxy_login: options.proxy_login.or(proxy_login),
111 | proxy_password: options.proxy_password.or(proxy_password),
112 | ..options
113 | }
114 | }
115 |
116 | fn render_url(&mut self) -> Result {
117 | self
118 | .engine
119 | .add_raw_template(&format!("{}#/url", self.file), &self.request.request.url)?;
120 | let url = self.engine.render(&format!("{}#/url", self.file), &self.context)?;
121 | Ok(url)
122 | }
123 |
124 | fn render_method(&mut self) -> Result {
125 | self
126 | .engine
127 | .add_raw_template(&format!("{}#/method", self.file), &self.request.request.method)?;
128 | let method = self.engine.render(&format!("{}#/method", self.file), &self.context)?;
129 | Ok(method)
130 | }
131 |
132 | fn render_headers(&mut self) -> Result {
133 | let headers = HeaderMap::from_iter(
134 | self
135 | .engine
136 | .render_map(
137 | &format!("{}#/headers", self.file),
138 | &self.request.request.headers,
139 | &self.context,
140 | )?
141 | .iter()
142 | .map(|(key, value)| {
143 | (
144 | HeaderName::from_str(key).unwrap(),
145 | HeaderValue::from_str(value).unwrap(),
146 | )
147 | }),
148 | );
149 | Ok(headers)
150 | }
151 |
152 | fn render_queries(&mut self) -> Result> {
153 | let queries = self.engine.render_map(
154 | &format!("{}#/queries", self.file),
155 | &self.request.request.queries,
156 | &self.context,
157 | )?;
158 | Ok(queries)
159 | }
160 |
161 | fn render_body(&mut self) -> Result