首页 > 其他分享 >Rust 实战丨HTTPie

Rust 实战丨HTTPie

时间:2024-06-12 10:58:30浏览次数:11  
标签:实战 body url resp parse print let HTTPie Rust

概述

之前学习过《陈天·Rust 编程第一课 - 04|get hands dirty:来写个实用的 CLI 小工具》,学的时候迷迷糊糊。后来在系统学习完 Rust 后,重新回过头来看这个实战小案例,基本上都能掌握,并且有了一些新的理解。所以我决定以一个 Rust 初学者的角度,并以最新版本的 Rust(1.7.6)和 clap(4.5.1)来重新实现这个案例,期望能对 Rust 感兴趣的初学者提供一些帮助。

本文将实现的应用叫 HTTPie,HTTPie 是一个用 Python 编写的命令行 HTTP 客户端,其目标是使 CLI 与 web 服务的交互尽可能愉快。它被设计为一个 curlwget 的替代品,提供易于使用的界面和一些用户友好的功能,如 JSON 支持、语法高亮和插件。它对于测试、调试和通常与 HTTP 服务器或 RESTful API 进行交云的开发人员来说非常有用。

HTTPie 的一些关键特性包括:

  1. JSON 支持:默认情况下,HTTPie 会自动发送 JSON,并且可以轻松地通过命令行发送 JSON 请求体。
  2. 语法高亮:它会为 HTTP 响应输出提供语法高亮显示,使得结果更加易于阅读。
  3. 插件:HTTPie 支持插件,允许扩展其核心功能。
  4. 表单和文件上传:可以很容易地通过表单上传文件。
  5. 自定义 HTTP 方法和头部:可以发送任何 HTTP 方法的请求,自定义请求头部。
  6. HTTPS、代理和身份验证支持:支持 HTTPS 请求、使用代理以及多种 HTTP 身份验证机制。
  7. 流式上传和下载:支持大文件的流式上传和下载。
  8. 会话支持:可以保存和重用常用的请求和集合。

本文我们将实现其中的 125。我们会支持发送 GET 和 POST 请求,其中 POST 支持设置请求头和 JSON 数据。

在本文中,你可以学习到:

  • 如何用 clap 解析命令行参数。
  • 如何用 tokio 进行异步编程。
  • 如何用 reqwest 发送 HTTP 请求。
  • 如何用 colored 在终端输出带颜色的内容。
  • 如何用 jsonxf 美化 json 字符串。
  • 如何用 anyhow 配合 ? 进行错误传播。
  • 如何使用 HTTPie 来进行 HTTP 接口测试。

在进行实际开发之前,推荐你先了解一下:

本文完整代码:hedon954/httpie

开发思路

HTTP 协议

回顾一下 HTTP 协议的请求体和响应体结构。

请求结构:

http request structure

响应结构:

http response structure

命令分析

在本文中,我们就实现 HTTPie cli 官方的这个示例:即允许指定请求方法、携带 headers 和 json 数据发送请求。

HTTPie 官方示例

我们来拆解一下,这个命令可以分为以下几个部分:

httpie <METHOD> <URL> [headers | params]...
  • <METHOD>: 请求方法,本案例中,我们仅支持 GET 和 POST。
  • <URL>: 请求地址。
  • <HEADERS>: 请求头,格式为 h1:v1
  • <PARAMS>: 请求参数,格式为 k1=v1,最终以 json 结构发送。

效果展示

➜  httpie git:(master) ✗ ./Httpie --help                                              
Usage: Httpie <COMMAND>

Commands:
  get   
  post  
  help  Print this message or the help of the given subcommand(s)

Options:
  -h, --help     Print help
  -V, --version  Print version

其中 post 子命令:

Usage: Httpie post <URL> <BODY>...

Arguments:
  <URL>      Specify the url you wanna request to
  <BODY>...  Set the request body. Examples: headers: header1:value1 params: key1=value1

Options:
  -h, --help  Print help

请求示例:

httpie response demo

思路梳理

httpie 开发思路梳理

第 1 步:解析命令行参数

本案例中 httpie 支持 2 个子命令:

  • get 支持 url 参数
  • post 支持 url、body 参数,因为其中 headers 和 params 是变长的,我们统一用 Vec<String> 类型的 body 来接收,然后用 := 来区分它们。

第 2 步:发送请求

  1. 使用 reqwest 创建 http client;
  2. 设置 url;
  3. 设置 method;
  4. 设置 headers;
  5. 设置 params;
  6. 发送请求;
  7. 获取响应体。

第 3 步:打印响应

  1. 打印 http version 和 status,并使用 colored 赋予蓝色;
  2. 打印 response headers,并使用 colored 赋予绿色;
  3. 确定 content-type,如果是 json,我们就用 jsonxf 美化 json 串并使用 colored 赋予蓝绿色输出,如果是其他类型,这里我们就输出原文即可。

实战过程

1. 创建项目

cargo new httpie

2. 添加依赖

[package]
name = "httpie"
version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = "1.0.80"
clap = { version = "4.5.1", features = ["derive"] }
colored = "2.1.0"
jsonxf = "1.1.1"
mime = "0.3.17"
reqwest = { version = "0.11.24", features = ["json"] }
tokio = { version = "1.36.0", features = ["rt", "rt-multi-thread", "macros"] }
  • anyhow: 用于简化异常处理。
  • clap: 解析命令行参数。
  • colored: 为终端输出内容赋予颜色。
  • jsonxf: 美化 json 串。
  • mime: 提供了各种 Media Type 的类型封装。
  • reqwest: http 客户端。
  • tokio: 异步库,本案例种我们使用 reqwest 的异步功能。

3. 完整源码

// src/main.rs  为减小篇幅,省略了单元测试,读者可自行补充。
use std::collections::HashMap;
use reqwest::{Client, header, Response};
use std::str::FromStr;
use anyhow::anyhow;
use clap::{Args, Parser, Subcommand};
use colored::Colorize;
use mime::Mime;
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use reqwest::Url;

#[derive(Parser)]
#[command(version, author, about, long_about = None)]
struct Httpie {
    #[command(subcommand)]
    methods: Method,
}

#[derive(Subcommand)]
enum Method {
    Get(Get),
    Post(Post)
}

#[derive(Args)]
struct Get {
    #[arg(value_parser = parse_url)]
    url: String,
}

#[derive(Args)]
struct Post {
    /// Specify the url you wanna request to.
    #[arg(value_parser = parse_url)]
    url: String,

    /// Set the request body.
    /// Examples:
    ///     headers:
    ///         header1:value1
    ///     params:
    ///         key1=value1
    #[arg(required = true, value_parser = parse_kv_pairs)]
    body: Vec<KvPair>
}

#[derive(Debug, Clone)]
struct KvPair {
    k: String,
    v: String,
    t: KvPairType,
}

#[derive(Debug,Clone)]
enum KvPairType {
    Header,
    Param,
}

impl FromStr for KvPair {
    type Err = anyhow::Error;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let pair_type: KvPairType;
        let split_char = if s.contains(':') {
            pair_type = KvPairType::Header;
            ':'
        } else {
            pair_type = KvPairType::Param;
            '='
        };

        let mut split = s.split(split_char);
        let err = || anyhow!(format!("failed to parse pairs {}",s));
        Ok(Self {
            k: (split.next().ok_or_else(err)?).to_string(),
            v: (split.next().ok_or_else(err)?).to_string(),
            t: pair_type,
        })
    }
}

fn parse_url(s: &str) -> anyhow::Result<String> {
    let _url: Url = s.parse()?;
    Ok(s.into())
}

fn parse_kv_pairs(s: &str) -> anyhow::Result<KvPair> {
    Ok(s.parse()?)
}

async fn get(client: Client, args: &Get) -> anyhow::Result<()> {
   let resp = client.get(&args.url).send().await?;
    Ok(print_resp(resp).await?)
}

async fn post(client: Client, args: &Post) -> anyhow::Result<()> {
    let mut body = HashMap::new();
    let mut header_map = HeaderMap::new();
    for pair in args.body.iter() {
        match pair.t {
            KvPairType::Param =>  {body.insert(&pair.k, &pair.v);}
            KvPairType::Header => {
                if let Ok(name) = HeaderName::from_str(pair.k.as_str()) {
                    if let Ok(value) = HeaderValue::from_str(pair.v.as_str()) {
                        header_map.insert(name,value);
                    } else {
                        println!("Invalid header value for key: {}", pair.v);
                    }
                } else {
                    println!("Invalid header key: {}", pair.k);
                }
            }
        }
    }
    let resp = client.post(&args.url)
        .headers(header_map)
        .json(&body).send().await?;
    Ok(print_resp(resp).await?)
}

async fn print_resp(resp: Response) -> anyhow::Result<()> {
    print_status(&resp);
    print_headers(&resp);
    let mime = get_content_type(&resp);
    let body = resp.text().await?;
    print_body(mime, &body);
    Ok(())
}

fn print_status(resp: &Response) {
    let status = format!("{:?} {}", resp.version(), resp.status()).blue();
    println!("{}\n", status);
}

fn print_headers(resp: &Response) {
    for (k,v) in resp.headers() {
        println!("{}: {:?}", k.to_string().green(), v);
    }
    print!("\n");
}

fn print_body(mime: Option<Mime>, resp: &String) {
    match mime {
        Some(v) => {
            if v == mime::APPLICATION_JSON {
                println!("{}", jsonxf::pretty_print(resp).unwrap().cyan())
            }
        }
        _ => print!("{}", resp),
    }
}

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() -> anyhow::Result<()>{
    let httpie = Httpie::parse();
    let client = Client::new();
    let result = match httpie.methods {
        Method::Get(ref args) => get(client, args).await?,
        Method::Post(ref args) => post(client, args).await?,
    };
    Ok(result)
}

可以看到,即使算上 use 部分,总代码也不过160 行左右,Rust 的 clap 库在 CLI 开发上确实 yyds!

接下来我们来一一拆解这部分的代码,其中关于 clap 的部分我不会过多展开,刚兴趣的读者可以参阅:深入探索 Rust 的 clap 库:命令行解析的艺术

3.1 命令行解析

我们先从 main() 开始:

#[tokio::main]
async fn main() -> anyhow::Result<()>{
    let httpie = Httpie::parse();
    let client = Client::new();
    let result = match httpie.methods {
        Method::Get(ref args) => get(client, args).await?,
        Method::Post(ref args) => post(client, args).await?,
    };
    Ok(result)
}

我们希望使用 clap 的异步功能,所以使用了 async 关键字,同时加上了 tokio 提供的属性宏 #[tokio::main],用于设置异步环境。为了能够使用 ? 快速传播错误,我们设置返回值为 anyhow::Result<()>,本项目中我们不对错误进行过多处理,所以这种方式可以大大简化我们的错误处理过程。

main() 中我们使用 Httpie::parse() 解析命令行中的参数,使用 Client::new() 创建一个 http client,根据解析到的命令行参数,我们匹配子命令 methods,分别调用 get()post() 来发送 GET 和 POST 请求。

Httpie 的定义如下:

#[derive(Parser)]
#[command(version, author, about, long_about = None)]
struct Httpie {
    #[command(subcommand)]
    methods: Method,
}

#[derive(Parser)] 是一个过程宏(procedural macro),用于自动为结构体实现 clap::Parser trait。这使得该结构体可以用来解析命令行参数。

Httpie 中我们定义了子命令 Method

#[derive(Subcommand)]
enum Method {
    Get(Get),
    Post(Post)
}

#[derive(Subcommand)] 属性宏会自动为枚举派生一些代码,以便它可以作为子命令来解析命令行参数。目前支持 GetPost 两个子命令,它们分别接收 GetPost 参数:

#[derive(Args)]
struct Get {
    #[arg(value_parser = parse_url)]
    url: String,
}

#[derive(Args)]
struct Post {
    #[arg(value_parser = parse_url)]
    url: String,
  
    #[arg(value_parser = parse_kv_pairs)]
    body: Vec<KvPair>
}

#[derive(Args)] 属性宏表明当前 struct 是命令的参数,其中 Get 仅支持 url 参数,Post 支持 urlbody 参数。

url 参数我们使用 parse_url 函数来进行解析:

use reqwest::Url;
fn parse_url(s: &str) -> anyhow::Result<String> {
    let _url: Url = s.parse()?;
    Ok(s.into())
}

这里 reqwest::Url 已经实现了 FromStr trait,所以这里我们可以直接调用 s.parse() 来解析 url

body,因为我们期望 CLI 使用起来像:

httpie url header1:value1 param1=v1

body 就是 header1:value1 param1=v1,一对 kv 就代表着一个 header 或者 param,用 := 来区分。因为 kv 对的个数的变长的,所以我们使用 Vec<KvPair> 来接收 body 这个参数,并使用 parse_kv_pairs 来解析 kv 对。

KvPair 是我们自定义的类型:

#[derive(Debug, Clone)]
struct KvPair {
    k: String,
    v: String,
    t: KvPairType,
}

#[derive(Debug,Clone)]
enum KvPairType {
    Header,
    Param,
}

parse_kv_pairs 的实现如下:

fn parse_kv_pairs(s: &str) -> anyhow::Result<KvPair> {
    Ok(s.parse()?)
}

在这里,你可以在 parse_kv_pairs() 函数中,对 s 进行解析并返回 anyhow::Result<KvPair>。不过,更优雅,更统一的方式是什么呢?就是像 reqwest::Url 一样,为 KvPair 实现 FromStr trait,这样就可以直接调用 s.parse() 来进行解析了。

impl FromStr for KvPair {
    type Err = anyhow::Error;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        ...
    }
}

3.2 发送请求

参数解析完,就到了发送请求的地方了,这里使用 reqwest crate 就非常方便了,这里就不赘述了,具体可以参考:Rust reqwest 简明教程

async fn get(client: Client, args: &Get) -> anyhow::Result<()> { ... }
async fn post(client: Client, args: &Post) -> anyhow::Result<()> { ... }

3.3 打印响应

httpie response demo

响应分为 3 个部分:

  • print_status()
  • print_headers()
  • print_body()
async fn print_resp(resp: Response) -> anyhow::Result<()> {
    print_status(&resp);
    print_headers(&resp);
    let mime = get_content_type(&resp);
    let body = resp.text().await?;
    print_body(mime, &body);
    Ok(())
}

print_status() 比较简单,就是打印 HTTP 版本和响应状态码,然后我们使用 colored crate 的 blue() 使其在终端以蓝色输出。

fn print_status(resp: &Response) {
    let status = format!("{:?} {}", resp.version(), resp.status()).blue();
    println!("{}\n", status);
}

print_headers() 中,我们使用 green() 使 header_name 在终端以绿色输出。

fn print_headers(resp: &Response) {
    for (k,v) in resp.headers() {
        println!("{}: {:?}", k.to_string().green(), v);
    }
    print!("\n");
}

响应体的格式(Media Type)有很多,本案例中我们仅支持 application/json,所以在 print_body() 之前,我们需要先读取 response header 中的 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_resp() 中,对于 application/json,我们使用 jsonxf crate 对进行美化,并使用 cyan() 使其在终端以蓝绿色输出。对于其他类型,我们姑且照原文输出。

fn print_body(mime: Option<Mime>, resp: &String) {
    match mime {
        Some(v) => {
            if v == mime::APPLICATION_JSON {
                println!("{}", jsonxf::pretty_print(resp).unwrap().cyan())
            }
        }
        _ => print!("{}", resp),
    }
}

总结

在本文中,我们深入探讨了如何使用 Rust 语言来实现一个类似于 HTTPie 的命令行工具。这个过程包括了对 HTTP 协议的理解、命令行参数的解析、HTTP 客户端的创建和请求发送,以及对响应的处理和展示。通过本文,读者不仅能够获得一个实用的命令行工具,还能够学习到如何使用 Rust 的库来构建实际的应用程序,包括 clapreqwesttokiocolored 等。此外,文章也说明了在 Rust 中进行异步编程和错误处理的一些常见模式。尽管示例代码的错误处理较为简单,但它提供了一个良好的起点,开发者可以在此基础上进行扩展和改进,以适应更复杂的应用场景。

标签:实战,body,url,resp,parse,print,let,HTTPie,Rust
From: https://blog.csdn.net/Hedon954/article/details/139593468

相关文章

  • 计算机毕业设计项目推荐,32127 爬虫-自驾游搜索系统(开题答辩+程序定制+全套文案 )上万套
    目 录摘要1绪论1.1研究背景1.2爬虫技术1.3flask框架介绍21.4论文结构与章节安排32 自驾游搜索系统分析42.1可行性分析42.2系统流程分析42.2.1数据增加流程52.3.2数据修改流程52.3.3数据删除流程52.3系统功能分析52.3.1功能性分析62.......
  • 计算机毕业设计项目推荐,32006 node 中国传统节日介绍网站(开题答辩+程序定制+全套文案
    基于node.js中国传统节日介绍网站 摘 要随着科学技术的飞速发展,社会的方方面面、各行各业都在努力与现代的先进技术接轨,通过科技手段来提高自身的优势,中国传统节日介绍网站当然也不能排除在外。中国传统节日介绍网站是以实际运用为开发背景,运用软件工程原理和开发方法,采......
  • 计算机毕业设计项目推荐,29042 基于Web的医院护理管理系统的设计(开题答辩+程序定制+全
    摘 要随着科学技术的飞速发展,社会的方方面面、各行各业都在努力与现代的先进技术接轨,通过科技手段来提高自身的优势,医院当然也不例外。医院预约管理系统是以实际运用为开发背景,运用软件工程原理和开发方法,采用Java技术构建的一个管理系统。整个开发过程首先对软件系统进......
  • TimerWheel(计时轮)在Rust中的实现及源码解析
    计时器轮(TimerWheel),模拟时钟格式组成的高效计时器TimerWheel算法原理环形数据结构:TimerWheel,即时间轮,是一个环形的数据结构,类似于时钟的面,被等分为多个格子或槽位(slot)。槽位时间间隔:每个槽位代表一个固定的时间间隔,例如1毫秒、1秒等。这个时间间隔决定了定时器的精度。......
  • # RocketMQ 实战:模拟电商网站场景综合案例(六)
    RocketMQ实战:模拟电商网站场景综合案例(六)一、RocketMQ实战:项目公共类介绍1、ID生成器:IDWorker:Twitter雪花算法。在shop-common工程模块中,IDWorker.java是ID生成器公共类,运用Twitter雪花算法,自动生成项目ID,而不会存在重复现象。packagecom.itheima.utils......
  • python pywinauto自动化实战案例:输入账号密码及点击登录按钮
    代码示例在使用pywinauto来模拟输入账号密码及点击登录按钮时,你需要先定位到相应的输入框和按钮,然后执行相应的操作。以下是一个基本的示例代码,展示如何实现这一过程:frompywinautoimportApplicationimporttime#假设你的应用已经启动,如果是启动应用的话,使用.start(......
  • Rustdesk 自建服务器
    自建rustdesk远程终端服务器,解决稳定远程控制需求。1、购买腾讯云服务器,99一年2、修改登录终端用户,默认使用ubuntu账户登录,存在权限限制,不能够上传文件Ubuntu系统如何使用root用户登录实例?(https://cloud.tencent.com/document/product/213/17278#ubuntu-.E7.B3.BB.E7.......
  • 【机器学习】Qwen2大模型原理、训练及推理部署实战
    目录​​​​​​​一、引言二、模型简介2.1Qwen2 模型概述2.2Qwen2 模型架构三、训练与推理3.1Qwen2 模型训练3.2Qwen2 模型推理四、总结一、引言刚刚写完【机器学习】Qwen1.5-14B-Chat大模型训练与推理实战 ,阿里Qwen就推出了Qwen2,相较于Qwen1.5中0.5B......
  • 书生·浦语大模型实战营 第八节课 微调弱智吧(如果AI可以正确回答弱智吧的所有问题,人类
    读前感:第四节课也进行了简单的微调,但最终微调出来个傻子,这次再试试,看看如何进行改善。实际的应用场景中使用微调的应该不会特别多,毕竟开源大模型并不是小公司可以玩得起的。对于小公司,真正的微调有哪些场景呢?欢迎大家讨论。读后感:本节课是整个训练营的最后一份笔记。希望......
  • Go变量作用域精讲及代码实战
    关注作者,复旦AI博士,分享AI领域与云服务领域全维度开发技术。拥有10+年互联网服务架构、AI产品研发经验、团队管理经验,同济本复旦硕博,复旦机器人智能实验室成员,国家级大学生赛事评审专家,发表多篇SCI核心期刊学术论文,阿里云认证的资深架构师,项目管理专业人士,上亿营收AI产品研发负责......