在現代軟體開發中,整合第三方 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);
    }
}
.envOPENWEATHER_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"}