From b6df73a3ad1d8112f09b3f0339b799ffc5febf1a Mon Sep 17 00:00:00 2001 From: anonfedora Date: Mon, 17 Mar 2025 16:51:22 +0100 Subject: [PATCH] feat: Weather Retrieval --- src/backend/Cargo.toml | 4 ++ src/backend/main.rs | 51 +++++++++++++++++++ src/backend/src/main.rs | 37 +++++++++++--- src/backend/src/routes/mod.rs | 1 + src/backend/src/routes/weather.rs | 85 +++++++++++++++++++++++++++++++ 5 files changed, 172 insertions(+), 6 deletions(-) create mode 100644 src/backend/main.rs create mode 100644 src/backend/src/routes/weather.rs diff --git a/src/backend/Cargo.toml b/src/backend/Cargo.toml index 0e5de8e..276fcb6 100644 --- a/src/backend/Cargo.toml +++ b/src/backend/Cargo.toml @@ -5,6 +5,10 @@ edition = "2021" [dependencies] axum = { version = "0.8.1", features = []} +dotenv = "0.15.0" +reqwest = {version="0.12.14", features = ["json"]} serde = { version = "1.0.219", features = ["derive"]} serde_json = "1.0.140" tokio = { version = "1.44.1", features = ["macros", "rt-multi-thread"] } +tracing = "0.1.41" +tracing-subscriber = "0.3.19" diff --git a/src/backend/main.rs b/src/backend/main.rs new file mode 100644 index 0000000..5a5ec40 --- /dev/null +++ b/src/backend/main.rs @@ -0,0 +1,51 @@ +use axum::{ + routing::{get, post}, + Router, +}; +mod routes; + +use dotenv::dotenv; +use reqwest::Client; +use routes::{ + handle_create_name::create_name, + handle_formatted_name::formatted_name, + handle_name::say_name, + weather::{get_weather, AppState}, +}; +use std::env; +use std::sync::Arc; + +const BASE_URL: &str = "0.0.0.0:5000"; // base url for server + +#[tokio::main] +async fn main() { + dotenv().ok(); + + let base_url = env::var("BASE_URL").unwrap_or_else(|_| BASE_URL.to_string()); + let weather_api_key = env::var("WEATHER_API_KEY").expect("WEATHER_API_KEY must be set"); + + println!("Starting server on {}", base_url); + println!("Weather API key: {}", weather_api_key); + + let state = Arc::new(AppState { + client: Client::new(), + api_key: weather_api_key, + }); + + let app = server(state); + + // run our server with hyper, listening globally on port 3000 + let listener = tokio::net::TcpListener::bind(base_url).await.unwrap(); + axum::serve(listener, app).await.unwrap(); +} + +// Router +pub fn server(state: Arc) -> Router { + Router::new() + .route("/", get(|| async { "Hello, World!" })) + .route("/say-name", get(|| say_name())) + .route("/json-name", get(|| formatted_name())) + .route("/create-name", post(create_name)) + .route("/weather", get(get_weather)) + .with_state(state) +} diff --git a/src/backend/src/main.rs b/src/backend/src/main.rs index e7f8cbe..5a5ec40 100644 --- a/src/backend/src/main.rs +++ b/src/backend/src/main.rs @@ -4,23 +4,48 @@ use axum::{ }; mod routes; +use dotenv::dotenv; +use reqwest::Client; use routes::{ - handle_create_name::create_name, handle_formatted_name::formatted_name, handle_name::say_name, + handle_create_name::create_name, + handle_formatted_name::formatted_name, + handle_name::say_name, + weather::{get_weather, AppState}, }; +use std::env; +use std::sync::Arc; + const BASE_URL: &str = "0.0.0.0:5000"; // base url for server #[tokio::main] async fn main() { + dotenv().ok(); + + let base_url = env::var("BASE_URL").unwrap_or_else(|_| BASE_URL.to_string()); + let weather_api_key = env::var("WEATHER_API_KEY").expect("WEATHER_API_KEY must be set"); + + println!("Starting server on {}", base_url); + println!("Weather API key: {}", weather_api_key); + + let state = Arc::new(AppState { + client: Client::new(), + api_key: weather_api_key, + }); + + let app = server(state); + // run our server with hyper, listening globally on port 3000 - let listener = tokio::net::TcpListener::bind(BASE_URL).await.unwrap(); - axum::serve(listener, server()).await.unwrap(); + let listener = tokio::net::TcpListener::bind(base_url).await.unwrap(); + axum::serve(listener, app).await.unwrap(); } // Router -pub fn server() -> Router { - return Router::new() +pub fn server(state: Arc) -> Router { + Router::new() .route("/", get(|| async { "Hello, World!" })) .route("/say-name", get(|| say_name())) .route("/json-name", get(|| formatted_name())) - .route("/create-name", post(create_name)); + .route("/create-name", post(create_name)) + .route("/weather", get(get_weather)) + .with_state(state) } diff --git a/src/backend/src/routes/mod.rs b/src/backend/src/routes/mod.rs index 923855f..6d497ef 100644 --- a/src/backend/src/routes/mod.rs +++ b/src/backend/src/routes/mod.rs @@ -1,3 +1,4 @@ pub mod handle_create_name; pub mod handle_formatted_name; pub mod handle_name; +pub mod weather; diff --git a/src/backend/src/routes/weather.rs b/src/backend/src/routes/weather.rs new file mode 100644 index 0000000..8e686e3 --- /dev/null +++ b/src/backend/src/routes/weather.rs @@ -0,0 +1,85 @@ +use axum::{ + extract::State, + response::{IntoResponse, Json}, +}; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use tokio::join; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WeatherResponse { + pub city: String, + pub temperature: Option, + pub conditions: Option, + pub error: Option, +} + +pub struct AppState { + pub client: Client, + pub api_key: String, +} + +pub async fn get_weather(State(state): State>) -> impl IntoResponse { + let cities = vec!["London", "Tokyo", "New York"]; + + // Create futures for each city weather request + let london_future = fetch_city_weather(&state, cities[0]); + let tokyo_future = fetch_city_weather(&state, cities[1]); + let newyork_future = fetch_city_weather(&state, cities[2]); + + //Execute all futures concurrently + let (london, tokyo, newyor) = join!(london_future, tokyo_future, newyork_future); + + // Combine the results into a single response + let results = vec![london, tokyo, newyor]; + Json(results) +} + +async fn fetch_city_weather(state: &Arc, city: &str) -> WeatherResponse { + let url = format!( + "https://api.weatherapi.com/v1/current.json?key={}&q={}&aqi=no", + state.api_key, city + ); + + match state.client.get(&url).send().await { + Ok(response) => { + if response.status().is_success() { + match response.json::().await { + Ok(json) => { + let temp = json["current"]["temp_c"].as_f64().map(|t| t as f32); + let cond = json["current"]["condition"]["text"] + .as_str() + .map(String::from); + + WeatherResponse { + city: city.to_string(), + temperature: temp, + conditions: cond, + error: None, + } + } + Err(e) => WeatherResponse { + city: city.to_string(), + temperature: None, + conditions: None, + error: Some(format!("Failed to parse JSON: {}", e)), + }, + } + } else { + WeatherResponse { + city: city.to_string(), + temperature: None, + conditions: None, + error: Some(format!("API error: {}", response.status())), + } + } + } + Err(e) => WeatherResponse { + city: city.to_string(), + temperature: None, + conditions: None, + error: Some(format!("Request failed: {}", e)), + }, + } +}