#[derive(clap::Args, Debug, Clone, Default)]
pub(crate) struct RunArgs {
pub(crate) path: Option<String>,
pub(crate) selector: Option<String>,
#[arg(long = "env", help = "Select a named collection environment")]
pub(crate) env: Option<String>,
#[arg(long)]
pub(crate) export: bool,
#[arg(long)]
pub(crate) benchmark: Option<usize>,
#[arg(short = 'v', long)]
pub(crate) verbose: bool,
#[arg(
long,
value_enum,
default_value_t = BodyMode::None,
help = "Show response bodies in text output for none, selected, failed, or all requests"
)]
pub(crate) body: BodyMode,
#[arg(long = "input", value_parser = parse_input_kv)]
pub(crate) inputs: Vec<(String, String)>,
#[arg(long, help = "Execute independent requests concurrently")]
pub(crate) parallel: bool,
#[arg(
long = "max-concurrency",
default_value_t = 0,
help = "Limit concurrent requests when running with --parallel"
)]
pub(crate) max_concurrency: usize,
#[arg(
long = "continue-on-error",
help = "Do not stop execution after the first request failure"
)]
pub(crate) continue_on_error: bool,
#[arg(
long,
help = "Fail instead of prompting for collection or request selection"
)]
pub(crate) non_interactive: bool,
#[arg(
long,
value_enum,
default_value_t = OutputFormat::Text,
help = "Render output as text, json, ndjson, or junit"
)]
pub(crate) output: OutputFormat,
}
#[derive(clap::Args, Debug, Clone)]
pub(crate) struct VerifyArgs {
pub(crate) path: String,
#[arg(
long,
value_enum,
default_value_t = OutputFormat::Text,
help = "Render output as text, json, ndjson, or junit"
)]
pub(crate) output: OutputFormat,
}
#[derive(clap::Args, Debug, Clone)]
pub(crate) struct InspectArgs {
pub(crate) path: String,
#[arg(
long,
value_enum,
default_value_t = InspectOutputFormat::Json,
help = "Render output as text, json, or ndjson"
)]
pub(crate) output: InspectOutputFormat,
}
#[derive(clap::Args, Debug, Clone)]
pub(crate) struct ImportArgs {
pub(crate) path: String,
pub(crate) selector: Option<String>,
#[arg(long, help = "Write the imported collection to the provided .hen path")]
pub(crate) output: String,
}
#[derive(clap::ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) enum OutputFormat {
#[default]
Text,
Json,
Ndjson,
Junit,
}
#[derive(clap::ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) enum BodyMode {
#[default]
None,
Selected,
Failed,
All,
}
#[derive(clap::ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) enum InspectOutputFormat {
Text,
#[default]
Json,
Ndjson,
}
impl InspectOutputFormat {
pub(crate) fn is_text(self) -> bool {
matches!(self, Self::Text)
}
}
impl BodyMode {
pub(crate) fn shows_selected(self) -> bool {
matches!(self, Self::Selected | Self::All)
}
pub(crate) fn shows_failed(self) -> bool {
matches!(self, Self::Failed | Self::All)
}
}
impl OutputFormat {
pub(crate) fn is_text(self) -> bool {
matches!(self, Self::Text)
}
pub(crate) fn as_str(self) -> &'static str {
match self {
Self::Text => "text",
Self::Json => "json",
Self::Ndjson => "ndjson",
Self::Junit => "junit",
}
}
}
#[derive(clap::Subcommand, Debug, Clone)]
pub(crate) enum Command {
Run(RunArgs),
Verify(VerifyArgs),
Inspect(InspectArgs),
Import(ImportArgs),
}
pub(crate) enum Invocation {
Run(RunArgs),
Verify(VerifyArgs),
Inspect(InspectArgs),
Import(ImportArgs),
}
#[derive(clap::Parser, Debug)]
#[command(name = "hen")]
#[command(version = env!("CARGO_PKG_VERSION"))]
#[command(about = "Command line API client.")]
#[command(
after_help = "Examples:\n hen run ./examples/lorem.hen all --non-interactive\n hen verify ./examples/lorem.hen\n hen inspect ./examples/graphql_protocol.hen --output json\n hen import ./examples/openapi_import.yaml --output ./examples/openapi_imported.hen"
)]
pub(crate) struct Cli {
#[command(subcommand)]
command: Option<Command>,
#[command(flatten)]
run: RunArgs,
}
impl Cli {
pub(crate) fn into_invocation(self) -> Invocation {
match self.command {
Some(Command::Run(args)) => Invocation::Run(args),
Some(Command::Verify(args)) => Invocation::Verify(args),
Some(Command::Inspect(args)) => Invocation::Inspect(args),
Some(Command::Import(args)) => Invocation::Import(args),
None => Invocation::Run(self.run),
}
}
}
pub(crate) struct CommandOutcome {
pub(crate) exit_code: i32,
}
impl CommandOutcome {
pub(crate) fn success() -> Self {
Self { exit_code: 0 }
}
pub(crate) fn with_exit_code(exit_code: i32) -> Self {
Self { exit_code }
}
}
fn parse_input_kv(s: &str) -> Result<(String, String), String> {
let mut parts = s.splitn(2, '=');
let key = parts
.next()
.map(str::trim)
.filter(|k| !k.is_empty())
.ok_or_else(|| "--input expects key=value".to_string())?;
let value = parts
.next()
.map(|v| v.trim().to_string())
.ok_or_else(|| "--input expects key=value".to_string())?;
Ok((key.to_string(), value))
}
#[cfg(test)]
mod tests {
use clap::Parser;
use super::*;
#[test]
fn cli_parses_run_subcommand_non_interactive_mode() {
let cli = Cli::try_parse_from(["hen", "run", "./example.hen", "all", "--non-interactive"])
.expect("cli should parse");
let Invocation::Run(args) = cli.into_invocation() else {
panic!("expected run invocation");
};
assert_eq!(args.path.as_deref(), Some("./example.hen"));
assert_eq!(args.selector.as_deref(), Some("all"));
assert!(args.non_interactive);
}
#[test]
fn cli_parses_verify_subcommand() {
let cli =
Cli::try_parse_from(["hen", "verify", "./example.hen"]).expect("cli should parse");
let Invocation::Verify(args) = cli.into_invocation() else {
panic!("expected verify invocation");
};
assert_eq!(args.path, "./example.hen");
}
#[test]
fn cli_parses_inspect_subcommand() {
let cli = Cli::try_parse_from(["hen", "inspect", "./example.hen", "--output", "ndjson"])
.expect("cli should parse");
let Invocation::Inspect(args) = cli.into_invocation() else {
panic!("expected inspect invocation");
};
assert_eq!(args.path, "./example.hen");
assert_eq!(args.output, InspectOutputFormat::Ndjson);
}
#[test]
fn cli_parses_import_subcommand() {
let cli = Cli::try_parse_from([
"hen",
"import",
"./spec.yaml",
"listPets",
"--output",
"./generated.hen",
])
.expect("cli should parse");
let Invocation::Import(args) = cli.into_invocation() else {
panic!("expected import invocation");
};
assert_eq!(args.path, "./spec.yaml");
assert_eq!(args.selector.as_deref(), Some("listPets"));
assert_eq!(args.output, "./generated.hen");
}
#[test]
fn cli_parses_run_output_format() {
let cli = Cli::try_parse_from(["hen", "run", "./example.hen", "--output", "json"])
.expect("cli should parse");
let Invocation::Run(args) = cli.into_invocation() else {
panic!("expected run invocation");
};
assert_eq!(args.output, OutputFormat::Json);
}
#[test]
fn cli_parses_run_environment() {
let cli = Cli::try_parse_from(["hen", "run", "./example.hen", "--env", "local"])
.expect("cli should parse");
let Invocation::Run(args) = cli.into_invocation() else {
panic!("expected run invocation");
};
assert_eq!(args.env.as_deref(), Some("local"));
}
#[test]
fn cli_parses_run_body_mode() {
let cli = Cli::try_parse_from(["hen", "run", "./example.hen", "--body", "selected"])
.expect("cli should parse");
let Invocation::Run(args) = cli.into_invocation() else {
panic!("expected run invocation");
};
assert_eq!(args.body, BodyMode::Selected);
}
}