在現代軟體開發中,整合第三方 API 服務是家常便飯。
今天我們要建立一個天氣查詢 API 客戶端,學習如何使用 Rust 呼叫外部 REST API,處理 JSON 回應,以及優雅地處理可能發生的錯誤。
-> (工程面)
-> 目標
cargo new weather_client
cd weather_client
[package]
name = "weather_client"
version = "0.1.0"
edition = "2021"
[dependencies]
reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
clap = { version = "4.0", features = ["derive"] }
dotenv = "0.15"
anyhow = "1.0"
簡單依賴介紹
reqwest
- HTTP 客戶端serde
- JSON 序列化/反序列化tokio
- 異步運行時clap
- 命令列參數解析dotenv
- 環境變數載入use serde::Deserialize;
#[derive(Deserialize, Debug)]
pub struct WeatherResponse {
pub main: MainWeather,
pub weather: Vec<Weather>,
pub name: String,
pub sys: Sys,
}
#[derive(Deserialize, Debug)]
pub struct MainWeather {
pub temp: f64,
pub feels_like: f64,
pub temp_min: f64,
pub temp_max: f64,
pub pressure: u32,
pub humidity: u32,
}
#[derive(Deserialize, Debug)]
pub struct Weather {
pub main: String,
pub description: String,
}
#[derive(Deserialize, Debug)]
pub struct Sys {
pub country: String,
}
use anyhow::{Context, Result};
use reqwest::Client;
pub struct WeatherClient {
client: Client,
api_key: String,
base_url: String,
}
impl WeatherClient {
pub fn new(api_key: String) -> Self {
Self {
client: Client::new(),
api_key,
base_url: "https://api.openweathermap.org/data/2.5".to_string(),
}
}
pub async fn get_weather(&self, city: &str, units: &str) -> Result<WeatherResponse> {
let url = format!(
"{}/weather?q={}&appid={}&units={}",
self.base_url, city, self.api_key, units
);
let response = self
.client
.get(&url)
.send()
.await
.context("Failed to send request")?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
return Err(anyhow::anyhow!(
"API request failed with status {}: {}",
status,
error_text
));
}
response
.json::<WeatherResponse>()
.await
.context("Failed to parse JSON response")
}
}
use anyhow::{Context, Result};
use reqwest::Client;
use serde::Deserialize;
#[derive(Deserialize, Debug)]
pub struct WeatherResponse {
pub main: MainWeather,
pub weather: Vec<Weather>,
pub name: String,
pub sys: Sys,
}
#[derive(Deserialize, Debug)]
pub struct MainWeather {
pub temp: f64,
pub feels_like: f64,
pub temp_min: f64,
pub temp_max: f64,
pub pressure: u32,
pub humidity: u32,
}
#[derive(Deserialize, Debug)]
pub struct Weather {
pub main: String,
pub description: String,
}
#[derive(Deserialize, Debug)]
pub struct Sys {
pub country: String,
}
pub struct WeatherClient {
client: Client,
api_key: String,
base_url: String,
}
impl WeatherClient {
pub fn new(api_key: String) -> Self {
Self {
client: Client::new(),
api_key,
base_url: "https://api.openweathermap.org/data/2.5".to_string(),
}
}
pub async fn get_weather(&self, city: &str, units: &str) -> Result<WeatherResponse> {
let url = format!(
"{}/weather?q={}&appid={}&units={}",
self.base_url, city, self.api_key, units
);
let response = self
.client
.get(&url)
.send()
.await
.context("Failed to send request")?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
return Err(anyhow::anyhow!(
"API request failed with status {}: {}",
status,
error_text
));
}
response
.json::<WeatherResponse>()
.await
.context("Failed to parse JSON response")
}
}
一樣我們用 clap
建立 cli
src/cli.rs
use clap::Parser;
#[derive(Parser)]
#[command(name = "weather")]
#[command(about = "A simple weather API client")]
pub struct Cli {
/// The city to get weather for
#[arg(short, long)]
pub city: String,
/// Temperature unit (metric, imperial, kelvin)
#[arg(short, long, default_value = "metric")]
pub units: String,
/// Display verbose output
#[arg(short, long)]
pub verbose: bool,
}
mod weather_client;
mod cli;
use anyhow::Result;
use clap::Parser;
use cli::Cli;
use weather_client::{WeatherClient, WeatherResponse};
#[tokio::main]
async fn main() -> Result<()> {
// 載入環境變數
dotenv::dotenv().ok();
let cli = Cli::parse();
// 從環境變數取得 API 金鑰
let api_key = std::env::var("OPENWEATHER_API_KEY")
.expect("Please set OPENWEATHER_API_KEY environment variable");
let client = WeatherClient::new(api_key);
match client.get_weather(&cli.city, &cli.units).await {
Ok(weather) => {
display_weather(&weather, &cli.units, cli.verbose);
}
Err(e) => {
eprintln!("Error: {}", e);
std::process::exit(1);
}
}
Ok(())
}
fn display_weather(weather: &WeatherResponse, units: &str, verbose: bool) {
let temp_unit = match units {
"metric" => "°C",
"imperial" => "°F",
_ => "K",
};
println!("🌤️ Weather in {}, {}", weather.name, weather.sys.country);
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
if let Some(weather_info) = weather.weather.first() {
println!("☁️ Condition: {} ({})", weather_info.main, weather_info.description);
}
println!("🌡️ Temperature: {:.1}{}", weather.main.temp, temp_unit);
println!("🤔 Feels like: {:.1}{}", weather.main.feels_like, temp_unit);
if verbose {
println!("📊 Detailed Information:");
println!(" • Min Temperature: {:.1}{}", weather.main.temp_min, temp_unit);
println!(" • Max Temperature: {:.1}{}", weather.main.temp_max, temp_unit);
println!(" • Pressure: {} hPa", weather.main.pressure);
println!(" • Humidity: {}%", weather.main.humidity);
}
}
.env
OPENWEATHER_API_KEY=your_api_key_here
# 基本查詢
cargo run -- --city "Taipei"
# 使用華氏溫度
cargo run -- --city "New York" --units imperial
# 顯示詳細資訊
cargo run -- --city "Tokyo" --verbose
# 查看幫助
cargo run -- --help
$ cargo run -- --city "InvalidCity"
Error: API request failed with status 404: {"cod":"404","message":"city not found"}