httpie源码剖析
example的使用
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
[[example]]
name = "cli"
[[example]]
name = "cli_verify"
[[example]]
name = "cli_get"
[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"
[[example]]
name = "cli"
[[example]]
name = "cli_verify"
[[example]]
name = "cli_get"
- 示例代码放在根目录的examples文件夹,与src同级
tree -L 2 ─╯
.
├── Cargo.toml
├── examples
│ ├── cli.rs
│ ├── cli_get.rs
│ └── cli_verify.rs
└── src
└── main.rs
2 directories, 5 files
- 执行指令
cargo run --example <example-name-in-cargo>
cargo run --example cli
cargo run --example cli_get
cargo run --example cli_verify
- 使用示例
cargo run --example cli ─╯
Finished dev [unoptimized + debuginfo] target(s) in 0.70s
Running `/Users/kuanhsiaokuo/Developer/spare_projects/rust_lab/geektime-rust/geektime_rust_codes/target/debug/examples/cli`
httpie 1.0
Tyr Chen <tyr@chen.com>
A naive httpie implementation with Rust, can you imagine how easy it is?
USAGE:
cli <SUBCOMMAND>
OPTIONS:
-h, --help Print help information
-V, --version Print version information
SUBCOMMANDS:
get feed get with an url and we will retrieve the response for you
help Print this message or the help of the given subcommand(s)
post feed post with an url and optional key=value pairs. We will post the data as JSON,
and retrieve the response for you
- Run a binary or example of the local package
- SUBCOMMANDS来自代码中的注释
Step1:指令解析
- 源码
use clap::Parser; // 定义 httpie 的 CLI 的主入口,它包含若干个子命令 // 下面 /// 的注释是文档,clap 会将其作为 CLI 的帮助 /// A naive httpie implementation with Rust, can you imagine how easy it is? #[derive(Parser, Debug)] #[clap(version = "1.0", author = "Tyr Chen <tyr@chen.com>")] struct Opts { #[clap(subcommand)] subcmd: SubCommand, } // 子命令分别对应不同的 HTTP 方法,目前只支持 get / post #[derive(Parser, Debug)] enum SubCommand { Get(Get), Post(Post), // 我们暂且不支持其它 HTTP 方法 } // get 子命令 /// feed get with an url and we will retrieve the response for you #[derive(Parser, Debug)] struct Get { /// HTTP 请求的 URL url: String, } // post 子命令。需要输入一个 url,和若干个可选的 key=value,用于提供 json body /// feed post with an url and optional key=value pairs. We will post the data /// as JSON, and retrieve the response for you #[derive(Parser, Debug)] struct Post { /// HTTP 请求的 URL url: String, /// HTTP 请求的 body body: Vec<String>, } fn main() { let opts: Opts = Opts::parse(); let opt_subcmd: SubCommand = opts.subcmd; // println!("{:?}", opts); println!("{:?}", opt_subcmd); // println!("{:?}", opts.subcmd); // 这里就可以看出,结构体的内在元素使用"."来获取 // println!("{:?}", opts::subcmd); }
clap::Parser相关资料
- clap的parser派生宏会自动实现parse方法来接收指令参数
struct Opts { #[clap(subcommand)] subcmd: SubCommand, } // 子命令分别对应不同的 HTTP 方法,目前只支持 get / post #[derive(Parser, Debug)]
fn main() { let opts: Opts = Opts::parse(); let opt_subcmd: SubCommand = opts.subcmd; // println!("{:?}", opts);
- 运行效果
cargo run --example cli get http://jsonplaceholder.typicode.com/posts/2 ─╯
Compiling httpie v0.1.0 (/Users/kuanhsiaokuo/Developer/spare_projects/rust_lab/geektime-rust/geektime_rust_codes/04_httpie)
Finished dev [unoptimized + debuginfo] target(s) in 2.31s
Running `/Users/kuanhsiaokuo/Developer/spare_projects/rust_lab/geektime-rust/geektime_rust_codes/target/debug/examples/cli get 'http://jsonplaceholder.typicode.com/posts/2'`
Opts { subcmd: Get(Get { url: "http://jsonplaceholder.typicode.com/posts/2" }) }
cargo run --example cli post http://jsonplaceholder.typicode.com/posts/2 ─╯
Finished dev [unoptimized + debuginfo] target(s) in 0.31s
Running `/Users/kuanhsiaokuo/Developer/spare_projects/rust_lab/geektime-rust/geektime_rust_codes/target/debug/examples/cli post 'http://jsonplaceholder.typicode.com/posts/2'`
Opts { subcmd: Post(Post { url: "http://jsonplaceholder.typicode.com/posts/2", body: [] }) }
cargo run --example cli delete http://jsonplaceholder.typicode.com/posts/2 ─╯
Finished dev [unoptimized + debuginfo] target(s) in 0.24s
Running `/Users/kuanhsiaokuo/Developer/spare_projects/rust_lab/geektime-rust/geektime_rust_codes/target/debug/examples/cli delete 'http://jsonplaceholder.typicode.com/posts/2'`
error: Found argument 'delete' which wasn't expected, or isn't valid in this context
USAGE:
cli <SUBCOMMAND>
For more information try --help
- opts的获取:自动以空格分隔,根据
模式匹配,之后的参数依次赋值给 struct里面的元素
Step2:添加参数验证与键值对改造
- 参数验证
/// 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, } // post 子命令。需要输入一个 url,和若干个可选的 key=value,用于提供 json body /// feed post with an url and optional key=value pairs. We will post the data /// as JSON, and retrieve the response for you #[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>, }
clap 允许你为每个解析出来的值添加自定义的解析函数,我们这里定义了parse_url和parse_kv_pair检查一下。
/// 因为我们为 KvPair 实现了 FromStr,这里可以直接 s.parse() 得到 KvPair 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()) }
- 键值对改造
/// 命令行中的 key=value 可以通过 parse_kv_pair 解析成 KvPair 结构 #[allow(dead_code)] #[derive(Debug)] struct KvPair { k: String, v: String, } /// 当我们实现 FromStr trait 后,可以用 str.parse() 方法将字符串解析成 KvPair 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(), }) } }
Step3:异步请求改造
Step3:异步请求改造
#[tokio::main] async fn main() -> Result<()> { let opts: Opts = Opts::parse(); // 生成一个 let client = Client::new(); match opts.subcmd { SubCommand::Get(ref args) => get(client, args).await?, SubCommand::Post(ref args) => post(client, args).await?, }; Ok(()) } async fn get(client: Client, args: &Get) -> Result<()> { let resp = client.get(&args.url).send().await?; println!("{:?}", resp.text().await?); Ok(()) } 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?; println!("{:?}", resp.text().await?); Ok(()) }
Step4: 语法高亮打印
Step4: 语法高亮打印
// 打印服务器版本号 + 状态码 fn print_status(resp: &Response) { let status = format!("{:?} {}", resp.version(), resp.status()).blue(); println!("{}\n", status); } // 打印服务器返回的 HTTP header fn print_headers(resp: &Response) { for (name, value) in resp.headers() { println!("{}: {:?}", name.to_string().green(), value); } println!(); } /// 打印服务器返回的 HTTP body 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(()) } /// 将服务器返回的 content-type 解析成 Mime 类型 fn get_content_type(resp: &Response) -> Option<Mime> { resp.headers() .get(header::CONTENT_TYPE) .map(|v| v.to_str().unwrap().parse().unwrap()) } 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); } }
/// 程序的入口函数,因为在 http 请求时我们使用了异步处理,所以这里引入 tokio #[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 = 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) }
Step5: 添加单元测试
Step5: 添加单元测试
// 仅在 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/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(), } ); } }