Skip to content

Commit

Permalink
Add --exit-code, --status, --headers, --no-body to CLI
Browse files Browse the repository at this point in the history
Closes #97
  • Loading branch information
LucasPickering committed Jan 24, 2024
1 parent d85b59d commit d52d4f2
Show file tree
Hide file tree
Showing 8 changed files with 111 additions and 24 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## [Unreleased] - ReleaseDate

### Added

- New informational flags to `slumber request`
- `--exit-status` to set exit code based on response status ([#97](https://github.com/LucasPickering/slumber/issues/97))
- `--status`, `--headers`, and `--no-body` to control printed output

## [0.12.1] - 2024-01-22

### Changed
Expand Down
12 changes: 12 additions & 0 deletions docs/src/user_guide/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,15 @@ You can use the `request` subcommand:
slumber request --profile production list_fishes
slumber rq -p production list_fishes # This is equivalent, just shorter
```

### Exit Code

By default, the CLI returns exit code 1 if there is a fatal error, e.g. the request failed to build or a network error occurred. If an HTTP response was received and parsed, the process will exit with code 0, regardless of HTTP status.

If you want to set the exit code based on the HTTP response status, use the flag `--exit-code`.

| Code | Reason |
| ---- | --------------------------------------------------- |
| 0 | HTTP response received |
| 1 | Fatal error |
| 2 | HTTP response had status >=400 (with `--exit-code`) |
5 changes: 3 additions & 2 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::{
GlobalArgs,
};
use async_trait::async_trait;
use std::process::ExitCode;

/// A CLI subcommand
#[derive(Clone, Debug, clap::Subcommand)]
Expand All @@ -29,12 +30,12 @@ pub enum CliCommand {
#[async_trait]
pub trait Subcommand {
/// Execute the subcommand
async fn execute(self, global: GlobalArgs) -> anyhow::Result<()>;
async fn execute(self, global: GlobalArgs) -> anyhow::Result<ExitCode>;
}

impl CliCommand {
/// Execute a non-TUI command
pub async fn execute(self, global: GlobalArgs) -> anyhow::Result<()> {
pub async fn execute(self, global: GlobalArgs) -> anyhow::Result<ExitCode> {
match self {
CliCommand::Request(command) => command.execute(global).await,
CliCommand::Import(command) => command.execute(global).await,
Expand Down
6 changes: 3 additions & 3 deletions src/cli/collections.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::{cli::Subcommand, db::Database, GlobalArgs};
use async_trait::async_trait;
use clap::Parser;
use std::path::PathBuf;
use std::{path::PathBuf, process::ExitCode};

/// View and modify request collection history
#[derive(Clone, Debug, Parser)]
Expand Down Expand Up @@ -29,7 +29,7 @@ enum CollectionsSubcommand {

#[async_trait]
impl Subcommand for CollectionsCommand {
async fn execute(self, _global: GlobalArgs) -> anyhow::Result<()> {
async fn execute(self, _global: GlobalArgs) -> anyhow::Result<ExitCode> {
let database = Database::load()?;
match self.subcommand {
CollectionsSubcommand::List => {
Expand All @@ -42,6 +42,6 @@ impl Subcommand for CollectionsCommand {
println!("Migrated {} into {}", from.display(), to.display());
}
}
Ok(())
Ok(ExitCode::SUCCESS)
}
}
5 changes: 3 additions & 2 deletions src/cli/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use std::{
fs::File,
io::{self, Write},
path::PathBuf,
process::ExitCode,
};

/// Generate a Slumber request collection from an external format
Expand All @@ -20,7 +21,7 @@ pub struct ImportCommand {

#[async_trait]
impl Subcommand for ImportCommand {
async fn execute(self, _global: GlobalArgs) -> anyhow::Result<()> {
async fn execute(self, _global: GlobalArgs) -> anyhow::Result<ExitCode> {
// Load the input
let collection = Collection::from_insomnia(&self.input_file)?;

Expand All @@ -41,6 +42,6 @@ impl Subcommand for ImportCommand {
};
serde_yaml::to_writer(&mut writer, &collection)?;

Ok(())
Ok(ExitCode::SUCCESS)
}
}
88 changes: 76 additions & 12 deletions src/cli/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,22 @@ use crate::{
use anyhow::{anyhow, Context};
use async_trait::async_trait;
use clap::Parser;
use dialoguer::{Input, Password};
use dialoguer::{console::Style, Input, Password};
use indexmap::IndexMap;
use itertools::Itertools;
use std::{error::Error, str::FromStr};
use reqwest::header::HeaderMap;
use std::{
error::Error,
fmt::{self, Display, Formatter},
process::ExitCode,
str::FromStr,
};

/// Exit code to return when `exit_status` flag is set and the HTTP response has
/// an error status code
const HTTP_ERROR_EXIT_CODE: u8 = 2;

/// Execute a single request
/// Execute a single request, and print its response
#[derive(Clone, Debug, Parser)]
#[clap(aliases=&["req", "rq"])]
pub struct RequestCommand {
Expand All @@ -26,12 +36,29 @@ pub struct RequestCommand {
#[clap(long = "profile", short)]
profile: Option<ProfileId>,

/// List of key=value overrides
/// Print HTTP response status
#[clap(long)]
status: bool,

/// Print HTTP request and response headers
#[clap(long)]
headers: bool,

/// Do not print HTTP response body
#[clap(long)]
no_body: bool,

/// Set process exit code based on HTTP response status. If the status is
/// <400, exit code is 0. If it's >=400, exit code is 2.
#[clap(long)]
exit_status: bool,

/// List of key=value template field overrides
#[clap(
long = "override",
short = 'o',
value_parser = parse_key_val::<String, String>,
)]
long = "override",
short = 'o',
value_parser = parse_key_val::<String, String>,
)]
overrides: Vec<(String, String)>,

/// Just print the generated request, instead of sending it
Expand All @@ -41,7 +68,7 @@ pub struct RequestCommand {

#[async_trait]
impl Subcommand for RequestCommand {
async fn execute(self, global: GlobalArgs) -> anyhow::Result<()> {
async fn execute(self, global: GlobalArgs) -> anyhow::Result<ExitCode> {
let collection_path = CollectionFile::try_path(global.collection)?;
let database = Database::load()?.into_collection(&collection_path)?;
let mut collection_file = CollectionFile::load(collection_path).await?;
Expand Down Expand Up @@ -89,15 +116,34 @@ impl Subcommand for RequestCommand {

if self.dry_run {
println!("{:#?}", request);
Ok(ExitCode::SUCCESS)
} else {
if self.headers {
eprintln!("{}", HeaderDisplay(&request.headers));
}

// Run the request
let http_engine = HttpEngine::new(database);
let record = http_engine.send(request).await?;
let status = record.response.status;

// Print stuff!
if self.status {
println!("{}", status.as_u16());
}
if self.headers {
println!("{}", HeaderDisplay(&record.response.headers));
}
if !self.no_body {
print!("{}", record.response.body.text());
}

// Print response
print!("{}", record.response.body.text());
if self.exit_status && status.as_u16() >= 400 {
Ok(ExitCode::from(HTTP_ERROR_EXIT_CODE))
} else {
Ok(ExitCode::SUCCESS)
}
}
Ok(())
}
}

Expand Down Expand Up @@ -140,3 +186,21 @@ where
.ok_or_else(|| format!("invalid key=value: no \"=\" found in `{s}`"))?;
Ok((key.parse()?, value.parse()?))
}

/// Wrapper making it easy to print a header map
struct HeaderDisplay<'a>(&'a HeaderMap);

impl<'a> Display for HeaderDisplay<'a> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let key_style = Style::new().bold();
for (key, value) in self.0 {
writeln!(
f,
"{}: {}",
key_style.apply_to(key),
value.to_str().unwrap_or("<invalid utf-8>")
)?;
}
Ok(())
}
}
5 changes: 3 additions & 2 deletions src/cli/show.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::{cli::Subcommand, util::Directory, GlobalArgs};
use async_trait::async_trait;
use clap::Parser;
use std::process::ExitCode;

/// Show meta information about slumber
#[derive(Clone, Debug, Parser)]
Expand All @@ -17,10 +18,10 @@ enum ShowTarget {

#[async_trait]
impl Subcommand for ShowCommand {
async fn execute(self, _global: GlobalArgs) -> anyhow::Result<()> {
async fn execute(self, _global: GlobalArgs) -> anyhow::Result<ExitCode> {
match self.target {
ShowTarget::Dir => println!("{}", Directory::root()),
}
Ok(())
Ok(ExitCode::SUCCESS)
}
}
6 changes: 3 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use crate::{
cli::CliCommand, collection::CollectionFile, tui::Tui, util::Directory,
};
use clap::Parser;
use std::{fs::File, path::PathBuf};
use std::{fs::File, path::PathBuf, process::ExitCode};
use tracing_subscriber::{filter::EnvFilter, prelude::*};

#[derive(Debug, Parser)]
Expand Down Expand Up @@ -45,7 +45,7 @@ struct GlobalArgs {
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
async fn main() -> anyhow::Result<ExitCode> {
// Global initialization
initialize_tracing().unwrap();
let args = Args::parse();
Expand All @@ -57,7 +57,7 @@ async fn main() -> anyhow::Result<()> {
let collection_path =
CollectionFile::try_path(args.global.collection)?;
Tui::start(collection_path).await?;
Ok(())
Ok(ExitCode::SUCCESS)
}

// Execute one request without a TUI
Expand Down

0 comments on commit d52d4f2

Please sign in to comment.