oreq 0.2.6

The tool for interactively creating curl arguments from OpenAPI.
Documentation
use anyhow::{anyhow, Result};
use http::Method;
use indoc::indoc;
use openapiv3::OpenAPI;
use promptuity::{themes::FancyTheme, Term};
use serde_json::{json, Value};
use std::error::Error;

use clap::{
    builder::{styling, Styles},
    Parser,
};

use crate::{
    error::AppError,
    fmt::{Formatter, RequestFormatter},
    prompt::Prompt,
};
use oreq::schema::read::ReadSchema;

#[derive(Parser, Debug)]
#[command(
    author,
    version,
    about,
    arg_required_else_help = true,
    styles = styles(),
    help_template = HELP_TEMPLATE
)]
pub struct Cli {
    #[arg(help = "OpenAPI schema path. Use a dash ('-') to read from standard input.", value_hint = clap::ValueHint::FilePath)]
    pub schema: String,
    #[arg(long, short, help = "Base URL", value_hint = clap::ValueHint::Url)]
    pub base_url: Option<String>,
    #[arg(long, short, help = "Path to request")]
    pub path: Option<String>,
    #[arg(long = "request", short = 'X', help = "Method to use")]
    pub method: Option<Method>,
    #[arg(long = "param", short = 'P', help = "Path parameters", value_parser = parse_body)]
    pub path_param: Option<Vec<(String, serde_json::Value)>>,
    #[arg(long, short, help = "Query parameters", value_parser = parse_body)]
    pub query_param: Option<Vec<(String, serde_json::Value)>>,
    #[arg(long, short = 'H', value_parser = parse_key_val)]
    pub headers: Option<Vec<(String, serde_json::Value)>>,
    #[arg(long, short, help = "Request body", value_parser = parse_body)]
    pub field: Option<Vec<(String, serde_json::Value)>>,
    #[arg(long = "format", help = "Output format", default_value = "curl")]
    pub fmt: Formatter,
}

fn parse_key_val(
    s: &str,
) -> Result<(String, serde_json::Value), Box<dyn Error + Send + Sync + 'static>> {
    let (key, value) = s.split_once(':').ok_or(anyhow!("Invalid format"))?;
    let key = key.trim();
    let value = value.trim();

    let key = key.to_string();
    let value = value.parse::<Value>().unwrap_or_else(|_| json!(value));

    Ok((key, value))
}

fn parse_body(
    s: &str,
) -> Result<(String, serde_json::Value), Box<dyn Error + Send + Sync + 'static>> {
    let (key, value) = s.split_once('=').ok_or(anyhow!("Invalid format"))?;
    let key = key.trim();
    let value = value.trim();

    let key = key.to_string();
    let value = value.parse::<Value>().unwrap_or_else(|_| json!(value));

    Ok((key, value))
}

fn styles() -> Styles {
    Styles::styled()
        .usage(styling::AnsiColor::Yellow.on_default() | styling::Effects::UNDERLINE)
        .header(styling::AnsiColor::Yellow.on_default() | styling::Effects::UNDERLINE)
        .literal(styling::AnsiColor::Green.on_default())
        .placeholder(styling::AnsiColor::Green.on_default())
}

const HELP_TEMPLATE: &str = indoc! {r#"
    {bin} v{version}
    {author}
    
    {about}

    {usage-heading}
    {tab}{usage}

    {all-args}
"#};

impl Cli {
    pub fn run(&self) -> Result<(), AppError> {
        let api = if self.schema == "-" {
            ReadSchema::<OpenAPI>::get_schema_from_stdin()
        } else {
            ReadSchema::<OpenAPI>::get_schema(self.schema.clone().into())
        }
        .map_err(|_| AppError::SchemaParseError)?;
        let server = self
            .base_url
            .clone()
            .or(api.schema.servers.first().map(|x| x.url.clone()))
            .ok_or(AppError::NoServers)?;

        let mut term = Term::default();
        let mut theme = FancyTheme::default();
        let mut init = Prompt::new(api.schema, &mut term, &mut theme).run(
            self.path.clone(),
            self.method.clone(),
            self.path_param
                .clone()
                .map(|x| x.into_iter().collect())
                .unwrap_or_default(),
            self.query_param
                .clone()
                .map(|x| x.into_iter().collect())
                .unwrap_or_default(),
            self.headers
                .clone()
                .map(|x| x.into_iter().collect())
                .unwrap_or_default(),
            self.field
                .clone()
                .map(|x| x.into_iter().collect())
                .unwrap_or_default(),
        )?;
        init.base = server;
        if let Some(headers) = self.headers.clone() {
            init.header.extend(headers);
        }
        let fmt: Box<dyn RequestFormatter> = self.fmt.clone().into();
        let out = fmt.format(&init)?;

        eprintln!();
        println!("{}", out);

        Ok(())
    }
}