Rust 实战丨HTTPie

news2024/12/29 9:22:48

概述

之前学习过《陈天·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 接口测试。

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

  • Rust reqwest 简明教程
  • Rust anyhow 简明教程
  • 深入探索 Rust 的 clap 库:命令行解析的艺术

本文完整代码: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 中进行异步编程和错误处理的一些常见模式。尽管示例代码的错误处理较为简单,但它提供了一个良好的起点,开发者可以在此基础上进行扩展和改进,以适应更复杂的应用场景。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1834563.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【C语言】解决C语言报错:Uninitialized Variable

文章目录 简介什么是Uninitialized VariableUninitialized Variable的常见原因如何检测和调试Uninitialized Variable解决Uninitialized Variable的最佳实践详细实例解析示例1&#xff1a;局部变量未初始化示例2&#xff1a;数组未初始化示例3&#xff1a;指针未初始化示例4&am…

Transformer革新:Infini-Transformer在长文本处理中的突破

在当今信息爆炸的时代&#xff0c;大型语言模型&#xff08;LLMs&#xff09;在处理长文本数据方面的需求日益增长。无论是科学研究、法律分析还是医学诊断&#xff0c;长文本的处理能力都显得尤为重要。然而&#xff0c;现有的基于Transformer的模型在处理这类数据时遇到了重大…

keepalived服务详解与实验 基于centos8

目录 keepalivedHA简介常用的高可用软件keepalived简介 keepalived常用模块keepalived功能简介keepalived常用文件keepalived配置文件详解keepalived实验1-上手环境准备安装服务主配置文件修改启动服务效果查看 keepalived脑裂1. 脑裂现象简介2. 脑裂的原因3. 脑裂的预防和解决…

【需求管理】软件需求开发和管理文档(原件Word)

1. 目的 2. 适用范围 3. 参考文件 4. 术语和缩写 5. 需求获取的方式 5.1. 与用户交谈向用户提问题 5.1.1. 访谈重点注意事项 5.1.2. 访谈指南 5.2. 参观用户的工作流程 5.3. 向用户群体发调查问卷 5.4. 已有软件系统调研 5.5. 资料收集 5.6. 原型系统调研 5.6.1. …

【数据结构】第十七弹---C语言实现选择排序

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】【C详解】 目录 1、选择排序 1.1、基本思想 1.2、代码实现 1.3、代码测试 1.4、时空复杂度分析 总结 1、选择排序 1.1、基本思想 选择排序是一种简单直观的比…

safari浏览器无法连接到服务器

问题&#xff1a;MacBook pro&#xff0c;网络连接正常&#xff0c;可以使用各种软件上网&#xff0c;唯独safari浏览器打不开网页&#xff0c;报错说Safari无法连接到服务器&#xff1b; 原因&#xff1a;使用了VPN&#xff0c;VPN自动更改了网络设置&#xff0c;导致Safari浏…

PythonPoc基础编写(3)---批量刷cnvd

文章目录 前言一、发现过程二、使用步骤1.引入库2.读入数据结果 总结 前言 想刷cnvd&#xff1f;最重要的是登录进行测试功能点 一、发现过程 找到一个网站 发现登录失败返回200 登录成功则是重定向 302 那就写一个脚本吧 二、使用步骤 1.引入库 import requests 2.读入…

【前端项目笔记】2 主页布局

主页布局 element-ui提供的组件名称就是它的类名 ☆☆ CSS选择器&#xff1a; &#xff08;1&#xff09;基本选择器 类型选择器 p/span/div…… 类选择器 (.classname) ID选择器 (#idname) 通配选择器 ( * ) &#xff08;2&#xff09;属性选择器 选择具有特定属性或属性值的…

掌控Linux-Conda环境安装终极指南

Linux-Conda环境安装教程 一、引言1.1. conda的作用与优势优势&#xff1a; 1.2. 简述conda在Linux系统中的重要性重要性&#xff1a; 二、准备工作2.1. 系统要求与兼容性Linux发行版支持情况硬件资源需求 2.2. 安装前的必要工具wget或curl的安装必要的开发库 三、下载与安装Mi…

计算机相关专业是否仍是“万金油”的选择?

亲爱的朋友们&#xff1a; 2024 年高考已然落幕&#xff0c;数百万高三学子站在了人生的重要十字路口&#xff0c;面临着选择大学专业这一关键抉择。在这个节点上&#xff0c;计算机相关专业是否还能被称为“万金油”的选择呢&#xff1f; 相信大家都知道&#xff0c;在最近这几…

Apache Doris 之 Docker 部署篇

前言 在现代数据驱动的商业环境中&#xff0c;实时数据分析和高并发查询能力是企业成功的关键因素之一。传统的数据仓库和分析工具在面对大规模数据处理和实时分析需求时&#xff0c;往往力不从心。Apache Doris 作为一个现代的 MPP 数据库管理系统&#xff0c;凭借其强大的查…

预埋螺栓抗滑移系数检测 内六角螺栓扭矩系数检测

螺栓检测范围&#xff1a;螺栓&#xff0c;高强螺栓&#xff0c;地脚螺栓&#xff0c;不锈钢螺栓&#xff0c;六角头螺栓&#xff0c;管片螺栓&#xff0c;膨胀螺栓&#xff0c;化学螺栓&#xff0c;镀锌螺栓&#xff0c;植筋螺栓&#xff0c;普通螺栓&#xff0c;钢结构螺栓&a…

【机器学习】CART决策树算法的核心思想及其大数据时代银行贷款参考案例——机器认知外界的重要算法

目录 引言 概述 CART决策树的特点 核心思想 减少不确定性的指标 基尼系数&#xff08;Gini Index&#xff09; 分类错误率 熵 银行实例 背景 数据准备 模型构建 模型评估与优化 应用与结果 代码示例 ✈✈✈✈引言✈✈✈✈ CART算法既可以用于分类问题&#xff0…

C# + easyui 写的一个web项目

用C# easyui 来开发&#xff0c;其实就是为了开发速度&#xff0c;用easyui可以一天写很多页面&#xff0c;比一些低代码平台还快。 登陆页面 主界面 记录数统计 家庭信息采集表 新建家庭 家庭成员 低保、五保人员帮扶情况登记表 低保、五保人员帮扶情况登记表的新增和编辑 治…

STM32学习笔记(五)--TIM输出比较PWM详解

&#xff08;1&#xff09;配置步骤1.配置RCC外设时钟 开启GPIO以及TIM外设2.配置时基单元的时钟 包含时钟源选择配置初始化时基单元3.配置输出比较单元 包含CCR的值 输出比较模式 极性选择 输出使能等4.配置GPIO口 初始化为复用式推挽输出的配置5.运行控制 启动计数器 输出PWM…

Java基础-案例练习-全是干货

目录 案例&#xff1a;卖飞机票 案例&#xff1a;找质数&#xff1a; 案例&#xff1a;开发验证码 案例&#xff1a;评委打分 案例&#xff1a;卖飞机票 package anlixunlian;import java.util.Scanner;/*机票价格按照淡季旺季、头等舱和经济舱收费、 输入机票原价、月份和…

Python进阶二: NumPy基础:数组和矢量计算

二、NumPy基础&#xff1a;数组和矢量计算 本文源自微博客(www.microblog.store),且以获得授权 NumPy&#xff08;Numerical Python的简称&#xff09;是Python数值计算最重要的基础包。大多数提供科学计算的包都是用NumPy的数组作为构建基础。 NumPy的部分功能如下&#xf…

SuiNS更新命名标准,增强用户体验

SuiNS将其面向用户的命名标准从 xxx.sui 更新为 xxx&#xff0c;让用户能够以一种适用于Web2和Web3世界的方式来代表自己。通过此更新&#xff0c;用户可以在其选择的名称前使用 &#xff0c;而不是在名称后添加 .sui。 Sui命名服务于去年推出&#xff0c;旨在使Sui上的地址更…

docker-compose harbor 2.11

harbor 前言 “Harbor” 是一个用于管理容器镜像的开源仓库项目。由 VMware 开发和维护,Harbor 提供一个企业级的 Docker 镜像仓库,具有丰富的功能,包括: 镜像管理:提供存储和分发 Docker 镜像的能力。安全性:支持镜像签名和漏洞扫描,确保镜像的安全性。身份认证:集成…