Rust LogoWeather CLI Tool

A Weather CLI (Command Line Interface) Tool is a command-line application designed to fetch and display current weather conditions or forecasts directly in the terminal. These tools are popular for their quick access to information, ease of integration into scripts, and minimal resource usage compared to graphical applications or websites.

Core Components and Functionality:

1. API Integration: The fundamental component is the integration with a weather API (e.g., OpenWeatherMap, AccuWeather, WeatherAPI.com). These APIs provide weather data in a structured format, typically JSON, for various locations.

2. HTTP Client: The tool uses an HTTP client library (like `reqwest` in Rust) to make GET requests to the chosen weather API's endpoints. The request usually includes parameters such as location (city name, ZIP code, latitude/longitude) and an API key.

3. API Key Management: To access most weather APIs, an API key is required for authentication and rate limiting. Best practice involves storing this key securely, often as an environment variable, rather than hardcoding it into the application.

4. Command-Line Argument Parsing: A CLI tool needs to accept input from the user, such as the desired city or an option for forecast. Libraries like `clap` in Rust simplify the parsing of command-line arguments and options.

5. Data Serialization/Deserialization: Once the HTTP client receives the API response (usually a JSON string), it needs to be deserialized into structured data (Rust structs) that the application can easily work with. Libraries like `serde` and `serde_json` are crucial for this.

6. Error Handling: Robust error handling is essential for network issues, invalid API keys, rate limits, non-existent locations, or malformed API responses.

7. Output Formatting: The fetched weather data needs to be presented to the user in a clear, concise, and readable format within the terminal. This often involves printing temperature, weather description, humidity, wind speed, etc.

Typical Use Cases:

* Quickly checking the weather without opening a browser.
* Integrating weather information into shell scripts or automated workflows.
* Learning about API consumption and CLI application development.

Benefits:

* Speed: Get weather updates almost instantly.
* Efficiency: Low resource consumption.
* Scriptability: Easily integrate into other programs or scripts.
* Universality: Works across different terminal environments.

Building a Weather CLI Tool is an excellent project for developers to practice working with external APIs, asynchronous programming, command-line argument parsing, and structured data handling.

Example Code

```rust
use clap::Parser;
use reqwest::Error as ReqwestError;
use serde::Deserialize;
use std::env;
use std::fmt;

// --- CLI Argument Parsing --- //
#[derive(Parser, Debug)]
#[command(author, version, about = "A simple weather CLI tool", long_about = None)]
struct Args {
    /// City name to get weather for (e.g., "London", "New York")
    #[arg(short, long)]
    city: String,
}

// --- Data Structures for API Response --- //

/// Represents the main weather information
#[derive(Deserialize, Debug)]
struct WeatherInfo {
    main: String,
    description: String,
    icon: String,
}

/// Represents the main temperature and humidity data
#[derive(Deserialize, Debug)]
struct MainData {
    temp: f32,
    feels_like: f32,
    temp_min: f32,
    temp_max: f32,
    pressure: u16,
    humidity: u8,
}

/// Represents wind data
#[derive(Deserialize, Debug)]
struct WindData {
    speed: f32,
    deg: u16,
}

/// Top-level structure for the OpenWeatherMap API response
#[derive(Deserialize, Debug)]
struct OpenWeatherResponse {
    weather: Vec<WeatherInfo>,
    main: MainData,
    wind: WindData,
    name: String,
    dt: u64, // Unix timestamp of data calculation
    timezone: i32, // Shift in seconds from UTC
}

// --- Custom Error Handling --- //
#[derive(Debug)]
enum WeatherCliError {
    Reqwest(ReqwestError),
    ApiError(String),
    EnvVarError(env::VarError),
    JsonError(serde_json::Error),
}

impl fmt::Display for WeatherCliError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            WeatherCliError::Reqwest(e) => write!(f, "Network error: {}", e),
            WeatherCliError::ApiError(msg) => write!(f, "API error: {}", msg),
            WeatherCliError::EnvVarError(e) => write!(f, "Environment variable error: {}", e),
            WeatherCliError::JsonError(e) => write!(f, "JSON parsing error: {}", e),
        }
    }
}

impl From<ReqwestError> for WeatherCliError {
    fn from(err: ReqwestError) -> WeatherCliError {
        WeatherCliError::Reqwest(err)
    }
}

impl From<env::VarError> for WeatherCliError {
    fn from(err: env::VarError) -> WeatherCliError {
        WeatherCliError::EnvVarError(err)
    }
}

impl From<serde_json::Error> for WeatherCliError {
    fn from(err: serde_json::Error) -> WeatherCliError {
        WeatherCliError::JsonError(err)
    }
}

// --- Main Logic --- //

/// Fetches weather data for a given city from OpenWeatherMap.
async fn get_weather(city: &str, api_key: &str) -> Result<OpenWeatherResponse, WeatherCliError> {
    let url = format!(
        "https://api.openweathermap.org/data/2.5/weather?q={}&units=metric&appid={}",
        city,
        api_key
    );

    let response = reqwest::get(&url).await?;

    if response.status().is_client_error() || response.status().is_server_error() {
        let error_msg = response.text().await?;
        return Err(WeatherCliError::ApiError(format!(
            "Failed to fetch weather: {}",
            error_msg
        )));
    }

    let weather_data: OpenWeatherResponse = response.json().await?;
    Ok(weather_data)
}

#[tokio::main]
async fn main() -> Result<(), WeatherCliError> {
    let args = Args::parse();

    // Retrieve API key from environment variables
    let api_key = env::var("OPENWEATHER_API_KEY")
        .map_err(|e| WeatherCliError::EnvVarError(e))
        .expect(
            "OPENWEATHER_API_KEY environment variable not set. 
            Please get one from openweathermap.org and set it."
        );

    println!("Fetching weather for {}...", args.city);

    match get_weather(&args.city, &api_key).await {
        Ok(data) => {
            println!("\n--- Weather in {} ---", data.name);
            if let Some(weather) = data.weather.first() {
                println!("Status: {} ({})", weather.main, weather.description);
            }
            println!("Temperature: {:.1}°C (Feels like: {:.1}°C)", data.main.temp, data.main.feels_like);
            println!("Min/Max Temp: {:.1}°C / {:.1}°C", data.main.temp_min, data.main.temp_max);
            println!("Humidity: {}%", data.main.humidity);
            println!("Wind Speed: {:.1} m/s", data.wind.speed);
            println!("Pressure: {} hPa", data.main.pressure);
            println!("-----------------------");
        }
        Err(e) => {
            eprintln!("Error: {}", e);
            std::process::exit(1);
        }
    }

    Ok(())
}
```

To run this code:

1.  Create a new Rust project:
    `cargo new weather_cli`
    `cd weather_cli`

2.  Add dependencies to `Cargo.toml`:
    ```toml
    [dependencies]
    clap = { version = "4.0", features = ["derive"] }
    reqwest = { version = "0.11", features = ["json"] }
    tokio = { version = "1", features = ["full"] }
    serde = { version = "1.0", features = ["derive"] }
    serde_json = "1.0"
    ```

3.  Replace `src/main.rs` with the provided code.

4.  Get an OpenWeatherMap API Key:
    *   Go to [openweathermap.org](https://openweathermap.org/).
    *   Sign up for a free account.
    *   Find your API key in your profile.

5.  Set the API key as an environment variable:
    *   Linux/macOS:
        `export OPENWEATHER_API_KEY="YOUR_API_KEY_HERE"`
    *   Windows (Command Prompt):
        `set OPENWEATHER_API_KEY="YOUR_API_KEY_HERE"`
    *   Windows (PowerShell):
        `$env:OPENWEATHER_API_KEY="YOUR_API_KEY_HERE"`
    *(Replace `YOUR_API_KEY_HERE` with your actual key. This is a temporary setting for the current terminal session. For persistent setting, refer to your OS documentation.)*

6.  Run the application:
    `cargo run -- --city "London"`
    or
    `cargo run -c "Tokyo"`