use crate::utils::precision::avg_f32;
use async_trait::async_trait;
use futures::future::join_all;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum WeatherError {
#[error("Failed to fetch weather data from provider: {0}")]
FetchError(String),
#[error("Failed to parse weather data: {0}")]
ParseError(String),
#[error("No reliable weather data could be obtained from any provider")]
NoReliableData,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WeatherData {
pub temperature_celsius: f32,
pub humidity_percent: f32,
pub wind_speed_kmh: f32,
pub precipitation_mm: f32,
pub weather_code: u32,
}
#[async_trait]
pub trait WeatherProvider: Send + Sync {
async fn get_weather(&self, latitude: f64, longitude: f64)
-> Result<WeatherData, WeatherError>;
fn provider_name(&self) -> &'static str;
}
pub struct WeatherEngine {
providers: Vec<Arc<dyn WeatherProvider>>,
}
impl WeatherEngine {
#[must_use]
pub fn new(providers: Vec<Arc<dyn WeatherProvider>>) -> Self {
Self { providers }
}
pub async fn fetch_and_validate(
&self,
latitude: f64,
longitude: f64,
) -> Result<WeatherData, WeatherError> {
if self.providers.is_empty() {
return Err(WeatherError::NoReliableData);
}
let futures = self
.providers
.iter()
.map(|p| p.get_weather(latitude, longitude));
let results = join_all(futures).await;
let successful_results: Vec<WeatherData> =
results.into_iter().filter_map(Result::ok).collect();
if successful_results.is_empty() {
return Err(WeatherError::NoReliableData);
}
let avg_temp = avg_f32(
&successful_results
.iter()
.map(|d| d.temperature_celsius)
.collect::<Vec<_>>(),
);
let avg_humidity = avg_f32(
&successful_results
.iter()
.map(|d| d.humidity_percent)
.collect::<Vec<_>>(),
);
let avg_wind = avg_f32(
&successful_results
.iter()
.map(|d| d.wind_speed_kmh)
.collect::<Vec<_>>(),
);
let avg_precip = avg_f32(
&successful_results
.iter()
.map(|d| d.precipitation_mm)
.collect::<Vec<_>>(),
);
let weather_code = successful_results
.iter()
.max_by_key(|d| d.weather_code)
.map_or(0, |d| d.weather_code);
Ok(WeatherData {
temperature_celsius: avg_temp,
humidity_percent: avg_humidity,
wind_speed_kmh: avg_wind,
precipitation_mm: avg_precip,
weather_code,
})
}
}
pub struct OpenMeteoProvider {
client: reqwest::Client,
}
impl Default for OpenMeteoProvider {
fn default() -> Self {
Self::new()
}
}
impl OpenMeteoProvider {
#[must_use]
pub fn new() -> Self {
Self {
client: reqwest::Client::new(),
}
}
}
#[derive(Deserialize)]
struct OpenMeteoResponse {
current_weather: OpenMeteoCurrent,
}
#[derive(Deserialize)]
struct OpenMeteoCurrent {
temperature: f32,
windspeed: f32,
weathercode: u32,
}
#[async_trait]
impl WeatherProvider for OpenMeteoProvider {
async fn get_weather(
&self,
latitude: f64,
longitude: f64,
) -> Result<WeatherData, WeatherError> {
let url = format!(
"https://api.open-meteo.com/v1/forecast?latitude={latitude}&longitude={longitude}¤t_weather=true"
);
let response = self
.client
.get(&url)
.send()
.await
.map_err(|e| WeatherError::FetchError(e.to_string()))?;
if !response.status().is_success() {
return Err(WeatherError::FetchError(format!(
"API returned status: {}",
response.status()
)));
}
let api_response = response
.json::<OpenMeteoResponse>()
.await
.map_err(|e| WeatherError::ParseError(e.to_string()))?;
Ok(WeatherData {
temperature_celsius: api_response.current_weather.temperature,
humidity_percent: 50.0, wind_speed_kmh: api_response.current_weather.windspeed,
precipitation_mm: 0.0, weather_code: api_response.current_weather.weathercode,
})
}
fn provider_name(&self) -> &'static str {
"Open-Meteo"
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockSunnyProvider;
#[async_trait]
impl WeatherProvider for MockSunnyProvider {
async fn get_weather(&self, _: f64, _: f64) -> Result<WeatherData, WeatherError> {
Ok(WeatherData {
temperature_celsius: 25.0,
humidity_percent: 40.0,
wind_speed_kmh: 10.0,
precipitation_mm: 0.0,
weather_code: 1, })
}
fn provider_name(&self) -> &'static str {
"Sunny"
}
}
struct MockRainyProvider;
#[async_trait]
impl WeatherProvider for MockRainyProvider {
async fn get_weather(&self, _: f64, _: f64) -> Result<WeatherData, WeatherError> {
Ok(WeatherData {
temperature_celsius: 15.0,
humidity_percent: 80.0,
wind_speed_kmh: 20.0,
precipitation_mm: 5.0,
weather_code: 61, })
}
fn provider_name(&self) -> &'static str {
"Rainy"
}
}
struct MockErrorProvider;
#[async_trait]
impl WeatherProvider for MockErrorProvider {
async fn get_weather(&self, _: f64, _: f64) -> Result<WeatherData, WeatherError> {
Err(WeatherError::FetchError(
"Simulated provider failure".to_string(),
))
}
fn provider_name(&self) -> &'static str {
"Error"
}
}
#[tokio::test]
async fn test_engine_with_single_provider() {
let providers: Vec<Arc<dyn WeatherProvider>> = vec![Arc::new(MockSunnyProvider)];
let engine = WeatherEngine::new(providers);
let result = engine.fetch_and_validate(0.0, 0.0).await.unwrap();
assert!((result.temperature_celsius - 25.0).abs() < f32::EPSILON);
}
#[tokio::test]
async fn test_engine_averages_multiple_providers() {
let providers: Vec<Arc<dyn WeatherProvider>> =
vec![Arc::new(MockSunnyProvider), Arc::new(MockRainyProvider)];
let engine = WeatherEngine::new(providers);
let result = engine.fetch_and_validate(0.0, 0.0).await.unwrap();
assert!((result.temperature_celsius - 20.0).abs() < f32::EPSILON);
assert!((result.humidity_percent - 60.0).abs() < f32::EPSILON);
assert_eq!(result.weather_code, 61);
}
#[tokio::test]
async fn test_engine_handles_failing_provider() {
let providers: Vec<Arc<dyn WeatherProvider>> = vec![
Arc::new(MockSunnyProvider),
Arc::new(MockErrorProvider), ];
let engine = WeatherEngine::new(providers);
let result = engine.fetch_and_validate(0.0, 0.0).await.unwrap();
assert!((result.temperature_celsius - 25.0).abs() < f32::EPSILON);
}
#[tokio::test]
async fn test_engine_fails_if_all_providers_fail() {
let providers: Vec<Arc<dyn WeatherProvider>> =
vec![Arc::new(MockErrorProvider), Arc::new(MockErrorProvider)];
let engine = WeatherEngine::new(providers);
let result = engine.fetch_and_validate(0.0, 0.0).await;
assert!(matches!(result, Err(WeatherError::NoReliableData)));
}
}