iskra 0.2.4

A safe, modern, Rust-native data transfer tool.
Documentation
use clap::{Parser, Subcommand, ValueEnum, Args};
use clap::{arg, command};

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)]
pub enum Method {
    Get,
    Post,
    Put,
    Delete,
    Patch,
    Options,
    Head,
    Trace,
    Connect,
}

#[derive(Args, Debug, Clone)]
pub struct GlobalOptions {
    /// Timeout in seconds (default: 30)
    #[arg(long, value_name = "seconds", default_value_t = 30)]
    pub timeout: u64,
    /// Disable automatic response decompression (gzip, deflate, brotli)
    #[arg(long, help = "Disable automatic response decompression (gzip, deflate, brotli)")]
    pub no_decompress: bool,
    /// Exit with non-zero code if response is not 2xx
    #[arg(long, help = "Exit with non-zero code if response is not 2xx")]
    pub fail: bool,
    /// Show response headers in output (global, use --show-headers)
    #[arg(long = "show-headers", global = true, help = "Always print response headers in output (global, use --show-headers)")]
    pub show_headers: bool,
    /// Show detailed error reports (can also use ISKRA_ERROR_VERBOSE=1)
    #[arg(long, help = "Show detailed error reports (same as ISKRA_ERROR_VERBOSE=1)")]
    pub verbose_errors: bool,
}

#[derive(Parser, Debug)]
#[command(
    name = "iskra",
    version,
    author = "Blake Park <blake@example.com>",
    about = "\x1b[1;31mIskra\x1b[0m: \x1b[1;31mSecure, modern, Rust-native data transfer tool\x1b[0m",
    long_about = "Iskra is a modern, secure, and flexible CLI for HTTP(S) data transfer.\n\n\
Global options can be placed before or after the subcommand and URL.\n\
\n\
Examples:\n\
    iskra get https://example.com --timeout 10\n\
    iskra --timeout 10 get https://example.com\n\
    iskra post https://httpbin.org/post 'hello world' --fail --no-decompress\n\
    iskra custom PATCH https://httpbin.org/patch --timeout 5 --verbose-errors\n\
    iskra delete https://example.com/resource --fail\n\
\n\
For more details, see: https://github.com/ParkBlake/iskra\n",
    help_template = "{about}\n\nUSAGE:\n    {usage}\n\nCOMMANDS:\n{commands}\n\nGLOBAL OPTIONS:\n{options}\n"
)]
pub struct Cli {
    #[command(flatten)]
    pub global: GlobalOptions,
    #[command(subcommand)]
    pub command: Commands,
}

#[derive(Subcommand, Debug)]
pub enum Commands {
    /// HTTP request with arbitrary method
    #[command(trailing_var_arg = true, about = "HTTP request with arbitrary method (e.g. PATCH, OPTIONS, etc.)")]
    Custom {
        /// HTTP method (e.g. PATCH, OPTIONS)
        #[arg(value_parser = crate::cli_parser::validate_method)]
        method: String,
        /// Target URL
        #[arg(value_parser = crate::cli_parser::validate_url)]
        url: String,
    /// Custom headers (key:value, per-request)
    #[arg(short = 'H', long, value_parser = crate::cli_parser::parse_header, num_args = 0.., value_name = "key:value", help = "Custom headers to include (per-request, use --header/-H)")]
        header: Vec<(String, String)>,
        /// Query parameters (key=value)
        #[arg(short = 'q', long, value_parser = crate::cli_parser::parse_query, num_args = 0.., value_name = "key=value", help = "Query parameters to include")]
        query: Vec<(String, String)>,
        /// Output file path
        #[arg(short, long, value_name = "file", value_hint = clap::ValueHint::FilePath, help = "Write response body to file")]
        output: Option<String>,
        /// Download a byte range (e.g. 1000-2000 or 1000-)
        #[arg(long, value_name = "start-end", value_parser = crate::cli_parser::parse_range, help = "Download a byte range")]
        range: Option<(u64, Option<u64>)>,
        /// Resume download if file exists (appends using HTTP Range)
        #[arg(long, help = "Resume download if file exists")]
        resume: bool,
        /// Request body (as trailing argument or --body)
        #[arg(long, value_name = "body", group = "body_input", help = "Request body (alternative to trailing argument)")]
        body: Option<String>,
        /// Trailing argument for request body (mutually exclusive with --body)
        #[arg(value_name = "body", group = "body_input", required = false, help = "Request body as trailing argument")]
        trailing_body: Vec<String>,
    },
    /// HTTP GET request
    Get {
        url: String,
    #[arg(short = 'H', long, value_parser = crate::cli_parser::parse_header, num_args = 0.., value_name = "key:value", help = "Custom headers to include (per-request, use --header/-H)")]
        header: Vec<(String, String)>,
        #[arg(short = 'q', long, value_parser = crate::cli_parser::parse_query, num_args = 0.., value_name = "key=value")]
        query: Vec<(String, String)>,
        #[arg(short, long, value_name = "file")]
        output: Option<String>,
        #[arg(long, value_name = "start-end", value_parser = crate::cli_parser::parse_range)]
        range: Option<(u64, Option<u64>)>,
        #[arg(long, help = "Resume download if file exists")]
        resume: bool,
    },
    /// HTTP POST request
    Post {
        url: String,
        body: String,
    #[arg(short = 'H', long, value_parser = crate::cli_parser::parse_header, num_args = 0.., value_name = "key:value", help = "Custom headers to include (per-request, use --header/-H)")]
        header: Vec<(String, String)>,
        #[arg(short = 'q', long, value_parser = crate::cli_parser::parse_query, num_args = 0.., value_name = "key=value")]
        query: Vec<(String, String)>,
        #[arg(short, long, value_name = "file")]
        output: Option<String>,
        #[arg(long, value_name = "start-end", value_parser = crate::cli_parser::parse_range)]
        range: Option<(u64, Option<u64>)>,
        #[arg(long, help = "Resume download if file exists")]
        resume: bool,
    },
    /// HTTP PUT request
    Put {
        url: String,
        body: String,
    #[arg(short = 'H', long, value_parser = crate::cli_parser::parse_header, num_args = 0.., value_name = "key:value", help = "Custom headers to include (per-request, use --header/-H)")]
        header: Vec<(String, String)>,
        #[arg(short = 'q', long, value_parser = crate::cli_parser::parse_query, num_args = 0.., value_name = "key=value")]
        query: Vec<(String, String)>,
        #[arg(short, long, value_name = "file")]
        output: Option<String>,
        #[arg(long, value_name = "start-end", value_parser = crate::cli_parser::parse_range)]
        range: Option<(u64, Option<u64>)>,
        #[arg(long, help = "Resume download if file exists")]
        resume: bool,
    },
    /// HTTP DELETE request
    Delete {
        url: String,
        #[arg(short = 'H', long, value_parser = crate::cli_parser::parse_header, num_args = 0.., value_name = "key:value")]
        header: Vec<(String, String)>,
        #[arg(short = 'q', long, value_parser = crate::cli_parser::parse_query, num_args = 0.., value_name = "key=value")]
        query: Vec<(String, String)>,
        #[arg(short, long, value_name = "file")]
        output: Option<String>,
        #[arg(long, value_name = "start-end", value_parser = crate::cli_parser::parse_range)]
        range: Option<(u64, Option<u64>)>,
        #[arg(long, help = "Resume download if file exists")]
        resume: bool,
    }
}