Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): command for creating requests #34

Merged
merged 4 commits into from
Nov 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ GET https://mhouge.dk/
The file can then be run using the following command:

```sh
hitt <PATH_TO_FILE>
hitt run <PATH_TO_FILE>
```

That is all that is need to send a request.
Expand Down Expand Up @@ -63,15 +63,15 @@ GET https://mhouge.dk/
By default, hitt does not exit on error status codes. That behavior can be changed by supplying the `--fail-fast` argument.

```sh
hitt --fail-fast <PATH_TO_FOLDER>
hitt run --fail-fast <PATH_TO_FOLDER>
```

### Running all files in directory

The `--recursive` argument can be passed to run all files in a directory:

```sh
hitt --recursive <PATH_TO_FOLDER>
hitt run --recursive <PATH_TO_FOLDER>
```

The order of each file execution is platform and file system dependent. That might change in the future, but for now you **should not** rely on the order.
Expand All @@ -81,23 +81,23 @@ The order of each file execution is platform and file system dependent. That mig
The `--hide-headers` argument can be passed to hide the response headers in the output:

```sh
hitt --hide-headers <PATH_TO_FILE>
hitt run --hide-headers <PATH_TO_FILE>
```

### Hiding response body

The `--hide-body` argument can be passed to hide the response body in the output:

```sh
hitt --hide-body <PATH_TO_FILE>
hitt run --hide-body <PATH_TO_FILE>
```

### Disabling pretty printing

The `--disable-formatting` argument can be passed to disable pretty printing of response body:

```sh
hitt --disable-formatting <PATH_TO_FILE>
hitt run --disable-formatting <PATH_TO_FILE>
```

## Disclaimer
Expand Down
4 changes: 4 additions & 0 deletions hitt-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
name = "hitt-cli"
version = "0.0.1"
edition = "2021"
description = "Command line HTTP testing tool focused on speed and simplicity"

[[bin]]
name = "hitt"
Expand All @@ -10,8 +11,11 @@ path = "src/main.rs"
[dependencies]
async-recursion = "1.0.5"
clap = { version = "4.3.24", features = ["derive"] }
console = "0.15.7"
hitt-formatter = { path = "../hitt-formatter" }
hitt-parser = { path = "../hitt-parser" }
hitt-request = { path = "../hitt-request" }
reqwest = "0.11.20"
shell-words = "1.1.0"
tempfile = "3.8.1"
tokio = { version = "1.32.0", features = ["fs", "macros", "rt-multi-thread"] }
2 changes: 2 additions & 0 deletions hitt-cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub(crate) mod new;
pub(crate) mod run;
148 changes: 148 additions & 0 deletions hitt-cli/src/commands/new.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
use std::str::FromStr;

use console::{Key, Term};
use hitt_parser::http::{HeaderName, HeaderValue, Uri};

use crate::{
config::NewCommandArguments,
terminal::{
editor::editor_input,
input::{confirm_input, select_input, text_input_prompt},
TEXT_RED, TEXT_RESET,
},
};

fn set_method(term: &Term) -> Result<String, std::io::Error> {
let http_methods = [
"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "CONNECT", "TRACE",
];

select_input(term, "Which HTTP method?", &http_methods)
}

fn set_url(term: &Term) -> Result<String, std::io::Error> {
text_input_prompt(
term,
"What should the url be?",
|input| !input.is_empty() && Uri::from_str(input).is_ok(),
|input| format!("{TEXT_RED}'{input}' is not a valid url{TEXT_RESET}"),
)
}

fn set_headers(term: &Term) -> Result<Vec<(String, String)>, std::io::Error> {
let mut headers = Vec::new();

let mut writing_headers =
confirm_input(term, "Do you want to add headers? (Y/n)", Key::Char('y'))?;

let key_validator = |input: &str| !input.is_empty() && HeaderName::from_str(input).is_ok();
let format_key_error =
|input: &str| format!("{TEXT_RED}'{input}' is not a valid header key{TEXT_RESET}");

let value_validator = |input: &str| !input.is_empty() && HeaderValue::from_str(input).is_ok();
let format_value_error =
|input: &str| format!("{TEXT_RED}'{input}' is not a valid header value{TEXT_RESET}");

while writing_headers {
let key = text_input_prompt(
term,
"What should the key be?",
key_validator,
format_key_error,
)?;

let value = text_input_prompt(
term,
"What should the value be?",
value_validator,
format_value_error,
)?;

headers.push((key, value));

writing_headers = confirm_input(
term,
"Do you want to add more headers? (Y/n)",
Key::Char('y'),
)?;
}

Ok(headers)
}

fn try_find_content_type(headers: &[(String, String)]) -> Option<&str> {
for (key, value) in headers {
if key.eq_ignore_ascii_case("content-type") {
return Some(value);
}
}

None
}

fn set_body(term: &Term, content_type: Option<&str>) -> Result<Option<String>, std::io::Error> {
if !confirm_input(term, "Do you want to add a body? (Y/n)", Key::Char('y'))? {
return Ok(None);
}

editor_input(term, content_type)
}

async fn save_request(
path: &std::path::Path,
method: String,
url: String,
headers: &[(String, String)],
body: Option<String>,
) -> Result<(), std::io::Error> {
let mut contents = format!("{method} {url}\n");

if !headers.is_empty() {
for (key, value) in headers {
contents.push_str(key);
contents.push_str(": ");
contents.push_str(value);
contents.push('\n');
}
}

if let Some(body) = body {
contents.push('\n');
contents.push_str(&body);
contents.push('\n');
}

tokio::fs::write(path, contents).await
}

async fn check_if_exist(term: &Term, path: &std::path::Path) -> Result<(), std::io::Error> {
if tokio::fs::try_exists(path).await? {
let should_continue = confirm_input(
term,
&format!("File '{path:?}' already exist, do you want to continue? (y/N)"),
Key::Char('n'),
)?;

if !should_continue {
std::process::exit(0);
}
}

Ok(())
}

pub(crate) async fn new_command(args: &NewCommandArguments) -> Result<(), std::io::Error> {
let term = console::Term::stdout();

check_if_exist(&term, &args.path).await?;

let method = set_method(&term)?;

let url = set_url(&term)?;

let headers = set_headers(&term)?;

let body = set_body(&term, try_find_content_type(&headers))?;

save_request(&args.path, method, url, &headers, body).await
}
24 changes: 24 additions & 0 deletions hitt-cli/src/commands/run.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use crate::{
config::RunCommandArguments,
fs::{handle_dir, handle_file, is_directory},
terminal::print_error,
};

pub(crate) async fn run_command(args: &RunCommandArguments) -> Result<(), std::io::Error> {
let http_client = reqwest::Client::new();

// TODO: figure out a way to remove this clone
let cloned_path = args.path.clone();

match is_directory(&args.path).await {
Ok(true) => handle_dir(&http_client, cloned_path, args).await,
Ok(false) => handle_file(&http_client, cloned_path, args).await,
Err(io_error) => {
print_error(format!(
"error checking if {:?} is a directory\n{io_error:#?}",
args.path
));
std::process::exit(1);
}
}
}
35 changes: 27 additions & 8 deletions hitt-cli/src/config/mod.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
#[derive(clap::Parser, Debug)]
#[clap(
author = "Mads Hougesen, mhouge.dk",
version,
about = "hitt is a command line HTTP testing tool focused on speed and simplicity."
)]
pub(crate) struct CliArguments {
use clap::{Args, Parser, Subcommand};

#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
#[command(propagate_version = true)]
pub(crate) struct Cli {
#[command(subcommand)]
pub command: Commands,
}

#[derive(Subcommand, Debug)]
pub(crate) enum Commands {
Run(RunCommandArguments),
New(NewCommandArguments),
}

/// Send request
#[derive(Args, Debug)]
pub(crate) struct RunCommandArguments {
/// Path to .http file, or directory if supplied with the `--recursive` argument
#[arg()]
pub(crate) path: String,
pub(crate) path: std::path::PathBuf,

/// Exit on error response status code
#[arg(long, default_value_t = false)]
Expand All @@ -29,3 +41,10 @@ pub(crate) struct CliArguments {
#[arg(long, short, default_value_t = false)]
pub(crate) recursive: bool,
}

/// Create new http request
#[derive(Args, Debug)]
pub(crate) struct NewCommandArguments {
#[arg()]
pub(crate) path: std::path::PathBuf,
}
10 changes: 5 additions & 5 deletions hitt-cli/src/fs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ use async_recursion::async_recursion;
use hitt_request::send_request;

use crate::{
config::CliArguments,
printing::{handle_response, print_error},
config::RunCommandArguments,
terminal::{handle_response, print_error},
};

async fn get_file_content(path: std::path::PathBuf) -> Result<String, std::io::Error> {
Expand All @@ -19,7 +19,7 @@ pub(crate) async fn is_directory(path: &std::path::Path) -> Result<bool, std::io
pub(crate) async fn handle_file(
http_client: &reqwest::Client,
path: std::path::PathBuf,
args: &CliArguments,
args: &RunCommandArguments,
) -> Result<(), std::io::Error> {
println!("hitt: running {path:?}");

Expand All @@ -44,7 +44,7 @@ pub(crate) async fn handle_file(
async fn handle_dir_entry(
http_client: &reqwest::Client,
entry: tokio::fs::DirEntry,
args: &CliArguments,
args: &RunCommandArguments,
) -> Result<(), std::io::Error> {
let metadata = entry.metadata().await?;

Expand All @@ -68,7 +68,7 @@ async fn handle_dir_entry(
pub(crate) async fn handle_dir(
http_client: &reqwest::Client,
path: std::path::PathBuf,
args: &CliArguments,
args: &RunCommandArguments,
) -> Result<(), std::io::Error> {
if !args.recursive {
print_error(format!(
Expand Down
44 changes: 9 additions & 35 deletions hitt-cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,45 +1,19 @@
use std::str::FromStr;

use clap::Parser;
use config::CliArguments;
use fs::{handle_dir, handle_file, is_directory};
use printing::print_error;
use commands::new::new_command;
use commands::run::run_command;
use config::{Cli, Commands};

mod commands;
mod config;
mod fs;
mod printing;

async fn run(
http_client: &reqwest::Client,
path: std::path::PathBuf,
args: &CliArguments,
) -> Result<(), std::io::Error> {
match is_directory(&path).await {
Ok(true) => handle_dir(http_client, path, args).await,
Ok(false) => handle_file(http_client, path, args).await,
Err(io_error) => {
print_error(format!(
"error checking if {path:?} is a directory\n{io_error:#?}"
));
std::process::exit(1);
}
}
}
mod terminal;

#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
let args = CliArguments::parse();

let http_client = reqwest::Client::new();
let cli = Cli::parse();

match std::path::PathBuf::from_str(&args.path) {
Ok(path) => run(&http_client, path, &args).await,
Err(parse_path_error) => {
print_error(format!(
"error parsing path {} as filepath\n{parse_path_error:#?}",
args.path
));
std::process::exit(1);
}
match &cli.command {
Commands::Run(args) => run_command(args).await,
Commands::New(args) => new_command(args).await,
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use hitt_formatter::ContentType;

use crate::printing::{TEXT_RESET, TEXT_YELLOW};
use crate::terminal::{TEXT_RESET, TEXT_YELLOW};

#[inline]
fn __print_body(body: &str) {
Expand Down
Loading