Skip to content
Draft
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
90 changes: 81 additions & 9 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,82 @@
use clap::Command;

pub fn get_cli_matches() -> clap::ArgMatches {
Command::new("glint")
.version("0.1.0")
.author("John Freeman")
.about("Send HTTP requests based on a request chain defined in TOML")
.allow_external_subcommands(true)
.get_matches()
use clap::{Args, Parser, Subcommand};

/// A CLI application for managing and executing HTTP requests or collections.
#[derive(Parser)]
#[command(version, about, long_about = None)]
pub struct Cli {
/// The primary command to execute.
#[command(subcommand)]
pub command: Commands,
}

/// Top-level commands for the CLI.
#[derive(Subcommand)]
pub enum Commands {
/// Work with request collections (grouped sets of HTTP requests).
Collections {
/// Subcommands related to request collections.
#[command(subcommand)]
command: CollectionCommands,
},
/// Work with individual HTTP requests.
Requests {
/// Subcommands related to individual HTTP requests.
#[command(subcommand)]
command: RequestCommands,
},
}

/// Subcommands for managing and executing request collections.
#[derive(Subcommand)]
pub enum CollectionCommands {
/// Execute an entire collection of requests.
Run {
/// The collection file containing the requests to execute.
collection: String,

/// Additional options for controlling output behavior.
#[command(flatten)]
output_options: OutputOptions,
},
}

/// Additional output options for fine-tuning the CLI's behavior.
#[derive(Args, Clone)]
pub struct OutputOptions {
/// Displays the HTTP headers in the output (disabled by default).
#[arg(short = 'h', long, default_value_t = false)]
pub show_headers: bool,

/// Suppresses the HTTP response status (enabled by default).
#[arg(short = 's', long, default_value_t = false)]
pub hide_status: bool,

/// Suppresses the HTTP response body (enabled by default).
#[arg(short = 'b', long, default_value_t = false)]
pub hide_body: bool,

/// Disables pretty-printing for the HTTP response (enabled by default).
#[arg(short = 'r', long, default_value_t = false)]
pub raw_output: bool,

/// Disables pre-output masking (enabled by default).
#[arg(short = 'm', long, default_value_t = false)]
pub disable_masking: bool,
}

/// Subcommands for managing and executing individual requests.
#[derive(Subcommand)]
pub enum RequestCommands {
/// Execute a specific request within a collection.
Run {
/// The collection file containing the requests.
collection: String,

/// The specific request to execute within the collection.
request: String,

/// Additional options for controlling output behavior.
#[command(flatten)]
output_options: OutputOptions,
},
}
65 changes: 26 additions & 39 deletions src/executor.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::cli::OutputOptions;
use crate::masking::mask_json;
use crate::options::Options;
use crate::request::{Dependencies, Dependency, Request, RequestBody};
use crate::resolvers::env_var_resolver::EnvVarResolver;
use crate::resolvers::one_password_resolver::{OnePasswordResolver, OnePasswordResolverError};
Expand Down Expand Up @@ -52,7 +52,6 @@ static ENV_FILES_CACHE: Lazy<Mutex<HashMap<String, HashMap<String, String>>>> =
#[derive(Debug)]
pub struct Executor {
requests: HashMap<String, Request>,
options: Options,
http: Client,
env_var_resolver: EnvVarResolver,
prompt_resolver: PromptResolver,
Expand All @@ -61,13 +60,12 @@ pub struct Executor {
}

impl Executor {
pub fn new(requests: Vec<Request>, options: Options) -> Self {
pub fn new(requests: Vec<Request>) -> Self {
Self {
requests: requests
.into_iter()
.map(|request| (request.name.clone(), request))
.collect(),
options,
http: Client::new(),
env_var_resolver: EnvVarResolver::new(),
prompt_resolver: PromptResolver::new(),
Expand All @@ -76,32 +74,17 @@ impl Executor {
}
}

pub async fn execute(&mut self) -> Result<(), Box<dyn std::error::Error>> {
match &self.options.request {
Some(request_name) => {
let request = self
.requests
.get(request_name)
.ok_or(ExecutionError::RequestNotFound {
request: request_name.to_owned(),
})?
.clone();

let response = self.execute_request(request.clone()).await?;

self.render_output(response).await?;
}
None => {
let cloned_requests: Vec<_> = self.requests.values().cloned().collect();

for request in cloned_requests {
let response = self.execute_request(request.clone()).await?;

self.render_output(response).await?;
}
}
}
Ok(())
pub async fn execute_request_named(
&mut self,
request: String,
) -> Result<Response, ExecutionError> {
let request = self
.requests
.get(&request)
.cloned()
.ok_or_else(|| ExecutionError::RequestNotFound { request })?;

self.execute_request(request).await
}

pub async fn execute_request(&mut self, request: Request) -> Result<Response, ExecutionError> {
Expand Down Expand Up @@ -220,9 +203,13 @@ impl Executor {
Ok(response)
}

async fn render_output(&mut self, response: Response) -> Result<(), ExecutionError> {
if !self.options.hide_status {
if self.options.raw_output {
pub async fn render_output(
&mut self,
response: Response,
options: OutputOptions,
) -> Result<(), ExecutionError> {
if !options.hide_status {
if options.raw_output {
println!(
"{}: {}",
response.status.as_str(),
Expand All @@ -249,10 +236,10 @@ impl Executor {
}
}

if self.options.show_headers {
if options.show_headers {
let mut headers = response.headers.clone();

if !self.options.disable_masking {
if !options.disable_masking {
for (_key, value) in &mut headers {
if let Some(value_str) = value.to_str().ok() {
let masked_value = mask_json(
Expand All @@ -266,7 +253,7 @@ impl Executor {
}
}

if self.options.raw_output {
if options.raw_output {
for (key, value) in headers {
println!(
"{}: {}",
Expand All @@ -290,16 +277,16 @@ impl Executor {
}
}

if !self.options.hide_body {
if !options.hide_body {
let mut body = serde_json::from_str::<serde_json::Value>(&response.text)
.map_err(|error| ExecutionError::Unknown(error.to_string()))?;

if !self.options.disable_masking {
if !options.disable_masking {
body = mask_json(body, &response.request.masking_rules)
.map_err(|error| ExecutionError::Unknown(error.to_string()))?;
}

if self.options.raw_output {
if options.raw_output {
println!(
"{}",
serde_json::to_string(&body)
Expand Down
43 changes: 38 additions & 5 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,57 @@
mod cli;
mod executor;
mod logging;
mod masking;
mod options;
mod request;
mod resolvers;
mod response;

use clap::Parser;
use cli::{Cli, CollectionCommands, Commands, RequestCommands};
use executor::Executor;
use logging::init_logging;
use options::Options;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let guard = init_logging()?;

let options = Options::parse();
match Cli::parse().command {
Commands::Collections { command } => match command {
CollectionCommands::Run {
collection,
output_options,
} => {
let requests = request::load_requests_from_toml(collection.as_str())?;

let cloned_requests: Vec<_> = requests.clone();

let mut executor = Executor::new(requests);

for request in cloned_requests {
let response = executor.execute_request(request.clone()).await?;

executor
.render_output(response, output_options.clone())
.await?;
}
}
},
Commands::Requests { command } => match command {
RequestCommands::Run {
collection,
request,
output_options,
} => {
let requests = request::load_requests_from_toml(collection.as_str())?;

let mut executor = Executor::new(requests);

let requests = request::load_requests_from_toml(options.collection.as_str())?;
let response = executor.execute_request_named(request).await?;

Executor::new(requests, options).execute().await?;
executor.render_output(response, output_options).await?;
}
},
}

drop(guard);

Expand Down
31 changes: 0 additions & 31 deletions src/options.rs

This file was deleted.