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