Web API 调用
查询 GitHub API
使用 reqwest::get
查询 点赞的用户 API v3,以获取某个 GitHub 项目的所有点赞用户的列表。使用 Response::json
将响应信息 reqwest::Response
反序列化为实现了 serde::Deserialize
trait 的 User
对象。
tokio::main 用于设置异步执行器,该进程异步等待 reqwest::get
完成,然后将响应信息反序列化到用户实例中。
use serde::Deserialize; use reqwest::Error; #[derive(Deserialize, Debug)] struct User { login: String, id: u32, } #[tokio::main] async fn main() -> Result<(), Error> { let request_url = format!("https://api.github.com/repos/{owner}/{repo}/stargazers", owner = "rust-lang-nursery", repo = "rust-cookbook"); println!("{}", request_url); let response = reqwest::get(&request_url).await?; let users: Vec<User> = response.json().await?; println!("{:?}", users); Ok(()) }
检查 API 资源是否存在
使用消息标头 HEAD 请求((Client::head
)查询 GitHub 用户端接口,然后检查响应代码以确定是否成功。这是一种无需接收 HTTP 响应消息主体,即可快速查询 rest 资源的方法。使用 ClientBuilder::timeout
方法配置的 reqwest::Client
结构体将确保请求不会超时。
由于 ClientBuilder::build
和 RequestBuilder::send
都返回错误类型 reqwest::Error
,所以便捷的 reqwest::Result
类型被用于主函数的返回类型。
use reqwest::Result; use std::time::Duration; use reqwest::ClientBuilder; #[tokio::main] async fn main() -> Result<()> { let user = "ferris-the-crab"; let request_url = format!("https://api.github.com/users/{}", user); println!("{}", request_url); let timeout = Duration::new(5, 0); let client = ClientBuilder::new().timeout(timeout).build()?; let response = client.head(&request_url).send().await?; if response.status().is_success() { println!("{} is a user!", user); } else { println!("{} is not a user!", user); } Ok(()) }
使用 GitHub API 创建和删除 Gist
使用 Client::post
创建一个 POST 请求提交到 GitHub gists API v3 接口的 gist,并使用 Client::delete
使用 DELETE 请求删除它。
reqwest::Client
负责这两个请求的详细信息,包括:URL、消息体(body)和身份验证。serde_json::json!
宏的 POST 主体可以提供任意形式的 JSON 主体,通过调用 RequestBuilder::json
设置请求主体,RequestBuilder::basic_auth
处理身份验证。本实例中调用 RequestBuilder::send
方法同步执行请求。
use error_chain::error_chain; use serde::Deserialize; use serde_json::json; use std::env; use reqwest::Client; error_chain! { foreign_links { EnvVar(env::VarError); HttpRequest(reqwest::Error); } } #[derive(Deserialize, Debug)] struct Gist { id: String, html_url: String, } #[tokio::main] async fn main() -> Result<()> { let gh_user = env::var("GH_USER")?; let gh_pass = env::var("GH_PASS")?; let gist_body = json!({ "description": "the description for this gist", "public": true, "files": { "main.rs": { "content": r#"fn main() { println!("hello world!");}"# } }}); let request_url = "https://api.github.com/gists"; let response = Client::new() .post(request_url) .basic_auth(gh_user.clone(), Some(gh_pass.clone())) .json(&gist_body) .send().await?; let gist: Gist = response.json().await?; println!("Created {:?}", gist); let request_url = format!("{}/{}",request_url, gist.id); let response = Client::new() .delete(&request_url) .basic_auth(gh_user, Some(gh_pass)) .send().await?; println!("Gist {} deleted! Status code: {}",gist.id, response.status()); Ok(()) }
实例中使用 HTTP 基本认证 为了授权访问 GitHub API。实际应用中或许将使用一个更为复杂的 OAuth 授权流程。
使用 RESTful API 分页
可以将分页的 web API 方便地包裹在 Rust 迭代器中,当到达每一页的末尾时,迭代器会从远程服务器加载下一页结果。
use reqwest::Result; use serde::Deserialize; #[derive(Deserialize)] struct ApiResponse { dependencies: Vec<Dependency>, meta: Meta, } #[derive(Deserialize)] struct Dependency { crate_id: String, } #[derive(Deserialize)] struct Meta { total: u32, } struct ReverseDependencies { crate_id: String, dependencies: <Vec<Dependency> as IntoIterator>::IntoIter, client: reqwest::blocking::Client, page: u32, per_page: u32, total: u32, } impl ReverseDependencies { fn of(crate_id: &str) -> Result<Self> { Ok(ReverseDependencies { crate_id: crate_id.to_owned(), dependencies: vec![].into_iter(), client: reqwest::blocking::Client::new(), page: 0, per_page: 100, total: 0, }) } fn try_next(&mut self) -> Result<Option<Dependency>> { if let Some(dep) = self.dependencies.next() { return Ok(Some(dep)); } if self.page > 0 && self.page * self.per_page >= self.total { return Ok(None); } self.page += 1; let url = format!("https://crates.io/api/v1/crates/{}/reverse_dependencies?page={}&per_page={}", self.crate_id, self.page, self.per_page); let response = self.client.get(&url).send()?.json::<ApiResponse>()?; self.dependencies = response.dependencies.into_iter(); self.total = response.meta.total; Ok(self.dependencies.next()) } } impl Iterator for ReverseDependencies { type Item = Result<Dependency>; fn next(&mut self) -> Option<Self::Item> { match self.try_next() { Ok(Some(dep)) => Some(Ok(dep)), Ok(None) => None, Err(err) => Some(Err(err)), } } } fn main() -> Result<()> { for dep in ReverseDependencies::of("serde")? { println!("reverse dependency: {}", dep?.crate_id); } Ok(()) }
处理速率受限 API
此实例使用 GitHub API - 速率限制展示如何处理远程服务器错误。本实例使用 hyper::header!
宏来解析响应头并检查 reqwest::StatusCode::Forbidden
。如果响应超过速率限制,则将等待并重试。
use error_chain::error_chain; use std::time::{Duration, UNIX_EPOCH}; use std::thread; use reqwest::StatusCode; error_chain! { foreign_links { Io(std::io::Error); Time(std::time::SystemTimeError); Reqwest(reqwest::Error); } } header! { (XRateLimitLimit, "X-RateLimit-Limit") => [usize] } header! { (XRateLimitRemaining, "X-RateLimit-Remaining") => [usize] } header! { (XRateLimitReset, "X-RateLimit-Reset") => [u64] } fn main() -> Result<()> { loop { let url = "https://api.github.com/users/rust-lang-nursery "; let client = reqwest::Client::new(); let response = client.get(url).send()?; let rate_limit = response.headers().get::<XRateLimitLimit>().ok_or( "response doesn't include the expected X-RateLimit-Limit header", )?; let rate_remaining = response.headers().get::<XRateLimitRemaining>().ok_or( "response doesn't include the expected X-RateLimit-Remaining header", )?; let rate_reset_at = response.headers().get::<XRateLimitReset>().ok_or( "response doesn't include the expected X-RateLimit-Reset header", )?; let rate_reset_within = Duration::from_secs(**rate_reset_at) - UNIX_EPOCH.elapsed()?; if response.status() == StatusCode::Forbidden && **rate_remaining == 0 { println!("Sleeping for {} seconds.", rate_reset_within.as_secs()); thread::sleep(rate_reset_within); return main(); } else { println!( "Rate limit is currently {}/{}, the reset of this limit will be within {} seconds.", **rate_remaining, **rate_limit, rate_reset_within.as_secs(), ); break; } } Ok(()) }