大家好,我是砸锅。一个摸鱼八年的后端开发。熟悉 Go、Lua。第十三天还是继续和大家一起学习 Rust😊
今天来完善之前那个 HTTP CLI 的小工具,毕竟学了那么久 Rust基础知识了,还是要结合项目来实操一下
先上代码:
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 = "4.1.4", features = ["derive"] }
colored = "2"
jsonxf = "1.1"
mime = "0.3"
reqwest = { version = "0.11.14", default-features = false, features = ["json"]}
tokio = { version = "1", features = ["full"]}
syntect = "5.0.0"
main.rs
extern crate clap;
use anyhow::{anyhow, Ok, 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 主入口,包含多个命令
// 下面 /// 的注释是文档, clap 会将其当成是 CLI 的帮助
/// A naive httpie implementation wite Rust, can you imagine how easy it is?
#[derive(Parser, Debug)]
struct Opts {
#[clap(subcommand)]
subcmd: SubCommand,
}
/// 子命令分别对应不同的 HTTP 方法,暂时只支持 GET / POST 方法
#[derive(Parser, Debug)]
enum SubCommand {
Get(Get),
Post(Post),
}
#[derive(Parser, Debug)]
struct Get {
#[arg(value_parser=parse_url)]
url: String,
}
#[derive(Parser, Debug)]
struct Post {
#[arg(value_parser=parse_url)]
url: String,
#[arg(value_parser=parse_kv_pair)]
body: Vec<KvPair>,
}
#[derive(Debug, Clone, 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> {
// check 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?)
}
#[tokio::main]
async fn main() -> Result<()> {
let opts = Opts::parse();
let mut headers = header::HeaderMap::new();
// add default headers
headers.insert("X-POWERED-BY", "Rust".parse()?);
headers.insert(header::USER_AGENT, "Rust Httpie".parse()?);
// create http client
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)
}
// print status code
fn print_status(resp: &Response) {
let status = format!("{:?} {}", resp.version(), resp.status()).blue();
println!("{}\n", status);
}
// print HTTP header
fn print_headers(resp: &Response) {
for (name, value) in resp.headers() {
println!("{}: {:?}", name.to_string().green(), value);
}
println!();
}
// print body
fn print_body(m: Option<Mime>, body: &str) {
match m {
Some(v) if v == mime::APPLICATION_JSON => print_syntect(body, "json"),
Some(v) if v == mime::TEXT_HTML => print_syntect(body, "html"),
_ => println!("{}", body),
};
}
// get content type
fn get_content_type(resp: &Response) -> Option<Mime> {
resp.headers()
.get(header::CONTENT_TYPE)
.map(|v| v.to_str().unwrap().parse().unwrap())
}
// print syntect
fn print_syntect(s: &str, ext: &str) {
// Load these once at the start of the 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(line, &ps).unwrap();
let escaped = as_24_bit_terminal_escaped(&ranges[..], true);
print!("{}", escaped);
}
}
// print response
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(())
}
// 仅在 cargo test 才编译
#[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/").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(),
}
);
}
}
執行效果如下:
➜ httpie git:(master) ✗ cargo run -- post http://httpbin.org/post greeting=hola name=Tyr
Compiling httpie v0.1.0 (/Users/xxx/rProject/httpie)
Finished dev [unoptimized + debuginfo] target(s) in 2.00s
Running `target/debug/httpie post 'http://httpbin.org/post' greeting=hola name=Tyr`
HTTP/1.1 200 OK
date: "Sat, 28 Jan 2023 07:32:59 GMT"
content-type: "application/json"
content-length: "502"
connection: "keep-alive"
server: "gunicorn/19.9.0"
access-control-allow-origin: "*"
access-control-allow-credentials: "true"
{
"args": {},
"data": "{\"name\":\"Tyr\",\"greeting\":\"hola\"}",
"files": {},
"form": {},
"headers": {
"Accept": "*/*",
"Content-Length": "32",
"Content-Type": "application/json",
"Host": "httpbin.org",
"User-Agent": "Rust Httpie",
"X-Amzn-Trace-Id": "Root=1-63d4cfab-4ab9c2842d7c32b06050d924",
"X-Powered-By": "Rust"
},
"json": {
"greeting": "hola",
"name": "Tyr"
},
"origin": "115.160.150.58",
"url": "http://httpbin.org/post"
}
➜ httpie git:(master) ✗ cargo test
Compiling httpie v0.1.0 (/Users/xxx/rProject/httpie)
Finished test [unoptimized + debuginfo] target(s) in 1.94s
Running unittests src/main.rs (target/debug/deps/httpie-e55ede2d4cd7199e)
running 2 tests
test tests::parse_kv_pair_works ... ok
test tests::parse_url_works ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s