use anyhow::{Context, Result, anyhow};
use axum::extract::{Json, Query};
use axum::http::StatusCode as AxumStatusCode;
use axum::routing::{Router, get};
use serde::{Deserialize, Serialize};
use wstd::http::{Client, Request, StatusCode, Uri};
const USER_AGENT: &str = "wstd-axum weather example (https://github.com/bytecodealliance/wstd)";
#[wstd_axum::http_server]
fn main() -> Router {
Router::new().route("/weather", get(weather))
}
#[derive(Serialize)]
struct LocationWeather {
location: Location,
weather: Weather,
}
async fn weather(
Query(query): Query<WeatherQuery>,
) -> axum::response::Result<Json<Vec<LocationWeather>>> {
if query.count == 0 {
Err((AxumStatusCode::BAD_REQUEST, "nonzero count required"))?;
}
let location_results = fetch_locations(&query)
.await
.context("searching for location")
.map_err(anyhow_response)?;
use futures_concurrency::future::TryJoin;
let results = location_results
.into_iter()
.map(|location| async move {
let weather = fetch_weather(&location)
.await
.with_context(|| format!("fetching weather for {}", location.qualified_name))?;
Ok::<_, anyhow::Error>(LocationWeather { location, weather })
})
.collect::<Vec<_>>()
.try_join()
.await
.map_err(anyhow_response)?;
Ok(Json(results))
}
#[derive(Deserialize)]
struct WeatherQuery {
city: String,
#[serde(default = "default_count")]
count: u32,
}
const fn default_count() -> u32 {
10
}
impl Default for WeatherQuery {
fn default() -> Self {
WeatherQuery {
city: "Portland".to_string(),
count: default_count(),
}
}
}
#[derive(Debug, Serialize)]
struct Location {
name: String,
qualified_name: String,
population: Option<u32>,
latitude: f64,
longitude: f64,
}
async fn fetch_locations(query: &WeatherQuery) -> Result<Vec<Location>> {
#[derive(Serialize)]
struct GeoQuery {
name: String,
count: u32,
language: String,
format: String,
}
let geo_query = GeoQuery {
name: query.city.clone(),
count: query.count,
language: "en".to_string(),
format: "json".to_string(),
};
let uri = Uri::builder()
.scheme("http")
.authority("geocoding-api.open-meteo.com")
.path_and_query(format!(
"/v1/search?{}",
serde_qs::to_string(&geo_query).context("serialize query string")?
))
.build()?;
let request = Request::get(uri)
.header("User-Agent", USER_AGENT)
.body(())?;
let resp = Client::new()
.send(request)
.await
.context("request to geocoding-api.open-meteo.com")
.context(AxumStatusCode::SERVICE_UNAVAILABLE)?;
if resp.status() != StatusCode::OK {
return Err(anyhow!("geocoding-api returned status {:?}", resp.status())
.context(AxumStatusCode::SERVICE_UNAVAILABLE));
}
#[derive(Deserialize)]
struct Contents {
results: Vec<Item>,
}
#[derive(Deserialize)]
struct Item {
name: String,
latitude: f64,
longitude: f64,
population: Option<u32>,
admin1: String,
admin2: Option<String>,
admin3: Option<String>,
admin4: Option<String>,
}
impl Item {
fn qualified_name(&self) -> String {
let mut n = String::new();
if let Some(name) = &self.admin4 {
n.push_str(name);
n.push_str(", ");
}
if let Some(name) = &self.admin3 {
n.push_str(name);
n.push_str(", ");
}
if let Some(name) = &self.admin2 {
n.push_str(name);
n.push_str(", ");
}
n.push_str(&self.admin1);
n
}
}
let contents: Contents = resp
.into_body()
.json()
.await
.context("parsing geocoding-api response")?;
let mut results = contents
.results
.into_iter()
.map(|item| {
let qualified_name = item.qualified_name();
Location {
name: item.name,
latitude: item.latitude,
longitude: item.longitude,
population: item.population,
qualified_name,
}
})
.collect::<Vec<_>>();
results.sort_by(|a, b| b.population.partial_cmp(&a.population).unwrap());
Ok(results)
}
#[derive(Debug, Serialize)]
struct Weather {
temp: f64,
temp_unit: String,
rain: f64,
rain_unit: String,
}
async fn fetch_weather(location: &Location) -> Result<Weather> {
#[derive(Serialize)]
struct ForecastQuery {
latitude: f64,
longitude: f64,
current: String,
}
let query = ForecastQuery {
latitude: location.latitude,
longitude: location.longitude,
current: "temperature_2m,rain".to_string(),
};
let uri = Uri::builder()
.scheme("http")
.authority("api.open-meteo.com")
.path_and_query(format!(
"/v1/forecast?{}",
serde_qs::to_string(&query).context("serialize query string")?
))
.build()?;
let request = Request::get(uri)
.header("User-Agent", USER_AGENT)
.body(())?;
let mut resp = Client::new()
.send(request)
.await
.context("request to api.open-meteo.com")
.context(AxumStatusCode::SERVICE_UNAVAILABLE)?;
if resp.status() != StatusCode::OK {
return Err(anyhow!("forecast api returned status {:?}", resp.status())
.context(AxumStatusCode::SERVICE_UNAVAILABLE));
}
#[derive(Deserialize)]
struct Contents {
current_units: Units,
current: Data,
}
#[derive(Deserialize)]
struct Units {
temperature_2m: String,
rain: String,
}
#[derive(Deserialize)]
struct Data {
temperature_2m: f64,
rain: f64,
}
let contents: Contents = resp.body_mut().json().await?;
let weather = Weather {
temp: contents.current.temperature_2m,
temp_unit: contents.current_units.temperature_2m,
rain: contents.current.rain,
rain_unit: contents.current_units.rain,
};
Ok(weather)
}
fn anyhow_response(e: anyhow::Error) -> axum::response::ErrorResponse {
let code = e
.downcast_ref::<AxumStatusCode>()
.cloned()
.unwrap_or(AxumStatusCode::INTERNAL_SERVER_ERROR);
(code, format!("{e:?}")).into()
}