Web API の呼び出し
GitHub API をクエリする
GitHub リポジトリにスターを付けたすべてのユーザーの一覧を取得するために、reqwest::get を使用して
[GitHub stargazers API v3][github-api-stargazers] をクエリします。
reqwest::Response は、serde::Deserialize を実装した User オブジェクトへデシリアライズされます。
このプログラムでは、GitHub パーソナルアクセストークンが環境変数 GITHUB_TOKEN に指定されていることを
想定しています。リクエストの設定には、[GitHub API][github-api] で必要とされる [reqwest::header::USER_AGENT]
ヘッダーが含まれています。プログラムは [serde_json::from_str] を使って
レスポンス本文を User オブジェクトのベクターへデシリアライズし、
レスポンスを処理して User インスタンスに変換します。
use serde::Deserialize;
use reqwest::Error;
use reqwest::header::USER_AGENT;
#[derive(Deserialize, Debug)]
struct User {
login: String,
id: u32,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let request_url = format!("https://api.github.com/repos/{owner}/{repo}/stargazers",
owner = "rust-lang-nursery",
repo = "rust-cookbook");
println!("{}", request_url);
let client = reqwest::blocking::Client::new();
let response = client
.get(request_url)
.header(USER_AGENT, "rust-web-api-client") // gh api では user-agent ヘッダーが必要
.send()?;
let users: Vec<User> = response.json()?;
println!("{:?}", users);
Ok(())
}
API リソースが存在するかどうかを確認する
HEAD リクエスト (Client::head) を使用して GitHub Users Endpoint をクエリし、その後レスポンスコードを調べて成功したかどうかを判定します。これは、ボディを受信することなく REST リソースをクエリするための手早い方法です。ClientBuilder::timeout で設定した reqwest::Client により、リクエストがタイムアウト時間を超えて継続しないことが保証されます。
ClientBuilder::build と [ReqwestBuilder::send] はどちらも reqwest::Error
型を返すため、main 関数の戻り値の型には短縮形の reqwest::Result を使用します。
use reqwest::Result;
use std::time::Duration;
use reqwest::ClientBuilder;
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 = reqwest::blocking::ClientBuilder::new().timeout(timeout).build()?;
let response = client.head(&request_url).send()?;
if response.status().is_success() {
println!("{} is a user!", user);
} else {
println!("{} is not a user!", user);
}
Ok(())
}
GitHub API を使用して Gist を作成および削除する
Client::post を使用して GitHub の [gists API v3][gists-api] に POST リクエストを送信し、Client::delete を使用して DELETE リクエストでそれを削除し、gist を作成します。
reqwest::Client は、URL、ボディ、認証を含む両方のリクエストの詳細を担当します。serde_json::json! マクロによる POST ボディは任意の JSON ボディを提供します。RequestBuilder::json の呼び出しでリクエストボディを設定します。RequestBuilder::basic_auth は認証を処理します。RequestBuilder::send の呼び出しによって、リクエストが同期的に実行されます。
use anyhow::Result;
use serde::Deserialize;
use serde_json::json;
use std::env;
use reqwest::Client;
#[derive(Deserialize, Debug)]
struct Gist {
id: String,
html_url: String,
}
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 = reqwest::blocking::Client::new()
.post(request_url)
.basic_auth(gh_user.clone(), Some(gh_pass.clone()))
.json(&gist_body)
.send()?;
let gist: Gist = response.json()?;
println!("Created {:?}", gist);
let request_url = format!("{}/{}",request_url, gist.id);
let response = reqwest::blocking::Client::new()
.delete(&request_url)
.basic_auth(gh_user, Some(gh_pass))
.send()?;
println!("Gist {} deleted! Status code: {}",gist.id, response.status());
Ok(())
}
この例では、GitHub API へのアクセスを認可するために HTTP Basic Auth を使用しています。一般的なユースケースでは、はるかに複雑な OAuth 認可フローのいずれかを使用することになるでしょう。
ページネーションされた RESTful API を利用する
ページネーションされた Web API を、使いやすい Rust イテレータでラップします。イテレータは遅延 評価され、各ページの末尾に達すると、リモートサーバーから次の結果ページを取得します。
// cargo-deps: reqwest="0.11", serde="1"
mod paginated {
use reqwest::Result;
use reqwest::header::USER_AGENT;
use serde::Deserialize;
#[derive(Deserialize)]
struct ApiResponse {
dependencies: Vec<Dependency>,
meta: Meta,
}
#[derive(Deserialize)]
pub struct Dependency {
pub crate_id: String,
pub id: u32,
}
#[derive(Deserialize)]
struct Meta {
total: u32,
}
pub struct ReverseDependencies {
crate_id: String,
dependencies: <Vec<Dependency> as IntoIterator>::IntoIter,
client: reqwest::blocking::Client,
page: u32,
per_page: u32,
total: u32,
}
impl ReverseDependencies {
pub 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);
println!("{}", url);
let response = self.client.get(&url).header(
USER_AGENT,
"cookbook agent",
).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<(), Box<dyn std::error::Error>> {
for dep in paginated::ReverseDependencies::of("serde")? {
let dependency = dep?;
println!("{} depends on {}", dependency.id, dependency.crate_id);
}
Ok(())
}