說到大名鼎鼎的tokio, 本站一部分代碼就是基於tokio的axum框架所編寫。筆者使用axum的主觀感受就是快速簡潔,少宏易學。這款插件也是基於tokio的, 大差不差, 應該也是不錯的。
cargo.toml部分
[package] name = "httpie" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] anyhow = "1" # 錯誤處理 clap = { version = "3", features = ["derive"] } # 命令行解析 colored = "2" # 命令終耑多彩顯示 jsonxf = "1.1" # JSON pretty print 格式化 mime = "0.3" # 處理 mime 類型 # reqwest 默認使用 openssl,有些 linux 用戶如果沒有安裝好 openssl 會無法編譯,這裡我改成了使用 rustls reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls"] } # HTTP 客戶耑 tokio = { version = "1", features = ["full"] } # 異步處理庫 syntect = "4"
main.rs部分
use anyhow::{anyhow, Result}; use clap::Parser; use colored::Colorize; use mime::Mime; use reqwest::{header, Client, Response, Url}; use std::{collections::HashMap, str::FromStr}; use syntect::{ easy::HighlightLines, highlighting::{Style, ThemeSet}, parsing::SyntaxSet, util::{as_24_bit_terminal_escaped, LinesWithEndings}, }; // 定義 httpie 的 CLI 的主入口,它包含若干個子命令 /// A naive httpie implementation with Rust, can you imagine how easy it is? #[derive(Parser, Debug)] #[clap(version = "1.0", author = "Your name")] struct Opts { #[clap(subcommand)] subcmd: SubCommand, } #[derive(Parser, Debug)] enum SubCommand { Get(Get), Post(Post), // 我們暫且不支持其它 HTTP 方法 } /// feed get with an url and we will retrieve the response for you #[derive(Parser, Debug)] struct Get { /// HTTP 請求的 URL #[clap(parse(try_from_str = parse_url))] url: String, } /// feed post with an url and optional key=value pairs. We will post the data #[derive(Parser, Debug)] struct Post { /// HTTP 請求的 URL #[clap(parse(try_from_str = parse_url))] url: String, /// HTTP 請求的 body #[clap(parse(try_from_str=parse_kv_pair))] body: Vec<KvPair>, } #[derive(Debug, PartialEq)] struct KvPair { k: String, v: String, } impl FromStr for KvPair { type Err = anyhow::Error; fn from_str(s: &str) -> Result<Self, Self::Err> { // 使用 = 進行 split,這會得到一個疊代器 let mut split = s.split('='); let err = || anyhow!(format!("Failed to parse {}", s)); Ok(Self { // 從疊代器中取第一個結果作爲 key,疊代器返回 Some(T)/None // 我們將其轉換成 Ok(T)/Err(E),然後用 ? 處理錯誤 k: (split.next().ok_or_else(err)?).to_string(), // 從疊代器中取第二個結果作爲 value v: (split.next().ok_or_else(err)?).to_string(), }) } } fn parse_kv_pair(s: &str) -> Result<KvPair> { s.parse() } fn parse_url(s: &str) -> Result<String> { // 這裡我們僅僅檢查一下 URL 是否合法 let _url: Url = s.parse()?; Ok(s.into()) } async fn get(client: Client, args: &Get) -> Result<()> { let resp = client.get(&args.url).send().await?; Ok(print_resp(resp).await?) } async fn post(client: Client, args: &Post) -> Result<()> { let mut body = HashMap::new(); for pair in args.body.iter() { body.insert(&pair.k, &pair.v); } let resp = client.post(&args.url).json(&body).send().await?; Ok(print_resp(resp).await?) } fn print_status(resp: &Response) { let status = format!("{:?} {}", resp.version(), resp.status()).blue(); println!("{}\n", status); } fn print_headers(resp: &Response) { for (name, value) in resp.headers() { println!("{}: {:?}", name.to_string().green(), value); } println!(); } fn print_body(m: Option<Mime>, body: &str) { match m { // 對於 "application/json" 我們 pretty print Some(v) if v == mime::APPLICATION_JSON => print_syntect(body, "json"), Some(v) if v == mime::TEXT_HTML => print_syntect(body, "html"), // 其它 mime type,我們就直接輸出 _ => println!("{}", body), } } async fn print_resp(resp: Response) -> Result<()> { print_status(&resp); print_headers(&resp); let mime = get_content_type(&resp); let body = resp.text().await?; print_body(mime, &body); Ok(()) } fn get_content_type(resp: &Response) -> Option<Mime> { resp.headers() .get(header::CONTENT_TYPE) .map(|v| v.to_str().unwrap().parse().unwrap()) } #[tokio::main] async fn main() -> Result<()> { let opts: Opts = Opts::parse(); let mut headers = header::HeaderMap::new(); // 爲我們的 http 客戶耑添加一些缺省的 HTTP 頭 headers.insert("X-POWERED-BY", "Rust".parse()?); headers.insert(header::USER_AGENT, "Rust Httpie".parse()?); let client = reqwest::Client::builder() .default_headers(headers) .build()?; let result = match opts.subcmd { SubCommand::Get(ref args) => get(client, args).await?, SubCommand::Post(ref args) => post(client, args).await?, }; Ok(result) } fn print_syntect(s: &str, ext: &str) { // Load these once at the start of your program let ps = SyntaxSet::load_defaults_newlines(); let ts = ThemeSet::load_defaults(); let syntax = ps.find_syntax_by_extension(ext).unwrap(); let mut h = HighlightLines::new(syntax, &ts.themes["base16-ocean.dark"]); for line in LinesWithEndings::from(s) { let ranges: Vec<(Style, &str)> = h.highlight(line, &ps); let escaped = as_24_bit_terminal_escaped(&ranges[..], true); print!("{}", escaped); } } #[cfg(test)] mod tests { use super::*; #[test] fn parse_url_works() { assert!(parse_url("abc").is_err()); assert!(parse_url("http://abc.xyz").is_ok()); assert!(parse_url("https://httpbin.org/post").is_ok()); } #[test] fn parse_kv_pair_works() { assert!(parse_kv_pair("a").is_err()); assert_eq!( parse_kv_pair("a=1").unwrap(), KvPair { k: "a".into(), v: "1".into() } ); assert_eq!( parse_kv_pair("b=").unwrap(), KvPair { k: "b".into(), v: "".into() } ); } }
編譯完之後在命令行下的使用方法:
主程序 a=1 b=2