#![cfg_attr(not(feature = "tui"), allow(dead_code, unused_imports))]
mod cli;
mod collection;
mod env_panel;
mod environment;
mod generators;
mod git_remote;
#[cfg(feature = "gui")]
mod gui;
mod http;
mod hurl;
mod i18n;
mod persistence;
mod postman;
mod postman_api;
mod postman_cli;
mod postman_flow;
mod postman_import;
mod probe;
mod remote_flow;
mod report;
mod report_cli;
#[cfg(any(feature = "tui", feature = "gui"))]
mod report_highlight;
mod request;
mod save_flow;
mod session;
mod shared_utils;
mod theme;
mod tree;
#[cfg(feature = "tui")]
mod tui;
mod vars_view;
mod workspace;
use clap::Parser;
#[cfg(feature = "tui")]
const DEFAULT_MODE: &str = "\x20 TUI (default) a terminal user interface\n";
#[cfg(all(not(feature = "tui"), feature = "gui"))]
const DEFAULT_MODE: &str =
"\x20 GUI (-g/--gui) a native graphical interface (this build has no TUI)\n";
#[cfg(all(not(feature = "tui"), not(feature = "gui")))]
const DEFAULT_MODE: &str = "";
#[cfg(feature = "tui")]
const DEFAULT_EXAMPLE: &str =
"\x20 paperboy Launch the terminal UI (default)\n";
#[cfg(all(not(feature = "tui"), feature = "gui"))]
const DEFAULT_EXAMPLE: &str = "\x20 paperboy --gui Launch the graphical UI\n";
#[cfg(all(not(feature = "tui"), not(feature = "gui")))]
const DEFAULT_EXAMPLE: &str = "";
static LONG_ABOUT: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| {
format!(
"PaperBoy — a Rust-native API client (a Postman alternative).\n\n\
Runs in one of these modes:\n\
{DEFAULT_MODE}\
\x20 CLI (-c/--collection) run a Hurl or Postman collection headlessly, then exit\n\
\x20 Report (-r/--report) run a PaperTrail report against a collection, then exit\n\
\x20 Import (--postman-import) download a Postman workspace over the API, then exit"
)
});
static AFTER_HELP: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| {
format!(
"Examples:\n\
{DEFAULT_EXAMPLE}\
\x20 paperboy -c collection.hurl Run a collection headlessly\n\
\x20 paperboy -c collection.hurl -e environment.vars Run a collection with an environment\n\
\x20 paperboy -c collection.hurl --batch Run as one batch (preserves cookies across requests)\n\
\x20 paperboy -c collection.hurl -e environment.vars -r report.trail Run a report\n\
\x20 paperboy -r report.trail Run a report, taking its collection/environment from the report's own headers\n\
\x20 paperboy -c collection.hurl -e prod.vars -e staging.vars -r report.trail Run a baseline/comparison report\n\
\x20 paperboy -c collection.hurl -r report.trail --dry-run Preview a report without sending anything\n\
\x20 paperboy -c collection.hurl -r report.trail -o out.csv Write the report to a file (- = stdout)\n\
\x20 paperboy -r report.trail -o out.html -o out.json One run, several formats\n\
\x20 paperboy -r report.trail --param CASES_DIR=./batch-07 Run a report, setting a PARAM it declares\n\
\x20 paperboy --postman-import List the Postman workspaces your API key can see\n\
\x20 paperboy --postman-import --postman-workspace ID -o ./API Download a whole Postman workspace\n\
\x20 paperboy --postman-import --postman-all -o ./API Download every workspace the key can see\n\n\
Environment (.vars) entries are KEY=value, where the value is a literal or a\n\
{{ ... }} provider reference resolved when the environment is loaded:\n\
\x20 Literal value USERNAME=demo\n\
\x20 Process env var BASE_URL={{ env:DEMO_BASE_URL }}\n\
\x20 1Password (op CLI) API_TOKEN={{ op://Vault/Item/field }}\n\
\x20 AWS SSM parameter DB_PASSWORD={{ ssm:/path/to/param }}\n\n\
Collections are Hurl files (.hurl) or Postman collection exports (.json);\n\
Postman JSON is imported automatically."
)
});
#[derive(Parser)]
#[command(
name = "paperboy",
version,
about = "PaperBoy — a Rust-native API client (a Postman alternative).",
long_about = LONG_ABOUT.as_str(),
after_help = AFTER_HELP.as_str()
)]
struct Cli {
#[arg(short = 'c', long, value_name = "FILE")]
collection: Option<String>,
#[arg(short = 'e', long, value_name = "FILE")]
env: Vec<String>,
#[arg(short = 'b', long)]
batch: bool,
#[arg(short = 'r', long, value_name = "FILE")]
report: Option<String>,
#[arg(long, requires = "report")]
dry_run: bool,
#[arg(long, value_name = "STEPS", value_delimiter = ',', requires = "report")]
targets: Vec<String>,
#[arg(long, value_name = "SEED", num_args = 0..=1, requires = "report")]
shuffle: Option<Option<u64>>,
#[arg(short = 'o', long = "output", value_name = "FILE")]
outputs: Vec<String>,
#[arg(
long = "param",
value_name = "NAME=VALUE",
value_parser = report::params::parse_assignment,
requires = "report"
)]
params: Vec<(String, String)>,
#[arg(short = 'g', long)]
gui: bool,
#[arg(long)]
postman_import: bool,
#[arg(long, value_name = "ID|URL")]
postman_workspace: Option<String>,
#[arg(long, conflicts_with = "postman_workspace")]
postman_all: bool,
#[arg(long, value_name = "KEY")]
postman_key: Option<String>,
#[arg(long, value_name = "WHAT")]
postman_what: Option<String>,
#[arg(long, value_name = "URL")]
postman_base_url: Option<String>,
#[arg(long, value_name = "FORMAT")]
postman_format: Option<String>,
#[arg(long)]
overwrite: bool,
}
fn main() {
let cli = Cli::parse();
if !cli.outputs.is_empty() && cli.report.is_none() && !cli.postman_import {
eprintln!("error: -o/--output requires -r/--report or --postman-import");
std::process::exit(2);
}
if cli.postman_import {
if cli.outputs.len() > 1 {
eprintln!(
"error: --postman-import downloads into one directory, but -o was given {} times",
cli.outputs.len()
);
std::process::exit(2);
}
std::process::exit(postman_cli::run(postman_cli::Args {
key: cli.postman_key,
workspace: cli.postman_workspace,
all: cli.postman_all,
out: cli.outputs.into_iter().next(),
what: cli.postman_what,
base_url: cli.postman_base_url,
format: cli.postman_format,
overwrite: cli.overwrite,
}));
}
if let Some(report) = cli.report {
std::process::exit(report_cli::run(
cli.collection,
cli.env,
report,
cli.outputs,
cli.dry_run,
cli.targets,
cli.shuffle,
cli.params.into_iter().collect(),
));
}
if let Some(collection) = cli.collection {
if cli.env.len() > 1 {
eprintln!(
"warning: multiple -e environments are only used by reports (-r); running the collection with the first one"
);
}
std::process::exit(cli::run(collection, cli.env.into_iter().next(), cli.batch));
}
if cli.gui {
std::process::exit(run_gui());
}
std::process::exit(run_tui());
}
#[cfg(feature = "tui")]
fn run_tui() -> i32 {
if let Err(e) = tui::run() {
eprintln!("tui error: {e}");
return 1;
}
0
}
#[cfg(all(not(feature = "tui"), feature = "gui"))]
fn run_tui() -> i32 {
eprintln!(
"This build of PaperBoy has no terminal UI.\n\
Pass `-g/--gui` for the graphical one, or `-c <collection.hurl>` or \
`-r <report.trail>` to run headlessly."
);
1
}
#[cfg(all(not(feature = "tui"), not(feature = "gui")))]
fn run_tui() -> i32 {
eprintln!(
"This build of PaperBoy is headless: it runs collections and reports, \
but has no user interface.\n\
Pass `-c <collection.hurl>` or `-r <report.trail>`, or reinstall with \
the terminal UI:\n\
\x20 cargo install paperboy --locked"
);
1
}
#[cfg(feature = "gui")]
fn run_gui() -> i32 {
if let Err(e) = gui::run() {
eprintln!("gui error: {e}");
return 1;
}
0
}
#[cfg(not(feature = "gui"))]
fn run_gui() -> i32 {
eprintln!(
"This build of PaperBoy has no GUI. Reinstall it with the `gui` feature:\n\
\x20 cargo install paperboy --locked --features gui"
);
1
}