说到大名鼎鼎的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