Split off all functionality to a library crate
This way we can build Rockets from outside the crate and run benchmarks, for example. * Add top-level `setup()` function to create a Rocket and set up the maps refresher task * Change the type of `maps::run` since `!` is still an unstable type * Fix HTTP code blocks in `README.md` so they don't appear as doctests to rustdoc
This commit is contained in:
parent
4576f8d90a
commit
0bf07bd134
10
README.md
10
README.md
|
@ -61,14 +61,14 @@ by providing a latitude and longitude.
|
||||||
For example, to get forecasts for all metrics for the Stationsplein in Utrecht,
|
For example, to get forecasts for all metrics for the Stationsplein in Utrecht,
|
||||||
use:
|
use:
|
||||||
|
|
||||||
```
|
```http
|
||||||
GET /forecast?address=Stationsplein,Utrecht&metrics[]=all
|
GET /forecast?address=Stationsplein,Utrecht&metrics[]=all
|
||||||
```
|
```
|
||||||
|
|
||||||
or directly by using its geocoded position:
|
or directly by using its geocoded position:
|
||||||
|
|
||||||
|
|
||||||
```
|
```http
|
||||||
GET /forecast?lat=52.0902&lon=5.1114&metrics[]=all
|
GET /forecast?lat=52.0902&lon=5.1114&metrics[]=all
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -80,7 +80,7 @@ When querying, the metrics need to be selected. It can be one of: `AQI`, `NO2`,
|
||||||
Note that the parameter "array" notation as well as the repeated parameter
|
Note that the parameter "array" notation as well as the repeated parameter
|
||||||
notation are supported. For example:
|
notation are supported. For example:
|
||||||
|
|
||||||
```
|
```http
|
||||||
GET /forecast?address=Stationsplein,Utrecht&metrics[]=AQI&metrics[]=pollen
|
GET /forecast?address=Stationsplein,Utrecht&metrics[]=AQI&metrics[]=pollen
|
||||||
GET /forecast?address=Stationsplein,Utrecht&metrics=AQI&metrics=pollen
|
GET /forecast?address=Stationsplein,Utrecht&metrics=AQI&metrics=pollen
|
||||||
GET /forecast?address=Stationsplein,Utrecht&metrics=all
|
GET /forecast?address=Stationsplein,Utrecht&metrics=all
|
||||||
|
@ -143,13 +143,13 @@ Currently, only the `PAQI`, `pollen` and `UVI` metrics are backed by a map.
|
||||||
For example, to get the current pollen map with a crosshair on Stationsplein in
|
For example, to get the current pollen map with a crosshair on Stationsplein in
|
||||||
Utrecht, use:
|
Utrecht, use:
|
||||||
|
|
||||||
```
|
```http
|
||||||
GET /map?address=Stationsplein,Utrecht&metric=pollen
|
GET /map?address=Stationsplein,Utrecht&metric=pollen
|
||||||
```
|
```
|
||||||
|
|
||||||
or directly by using its geocoded position:
|
or directly by using its geocoded position:
|
||||||
|
|
||||||
```
|
```http
|
||||||
GET /map?lat=52.0902&lon=5.1114&metric=pollen
|
GET /map?lat=52.0902&lon=5.1114&metric=pollen
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,275 @@
|
||||||
|
#![doc = include_str!("../README.md")]
|
||||||
|
#![warn(
|
||||||
|
clippy::all,
|
||||||
|
missing_debug_implementations,
|
||||||
|
rust_2018_idioms,
|
||||||
|
rustdoc::broken_intra_doc_links
|
||||||
|
)]
|
||||||
|
#![deny(missing_docs)]
|
||||||
|
|
||||||
|
use std::future::Future;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use rocket::http::ContentType;
|
||||||
|
use rocket::response::content::Custom;
|
||||||
|
use rocket::serde::json::Json;
|
||||||
|
use rocket::{get, routes, Build, Rocket, State};
|
||||||
|
|
||||||
|
pub(crate) use self::forecast::Metric;
|
||||||
|
use self::forecast::{forecast, Forecast};
|
||||||
|
pub(crate) use self::maps::{mark_map, Maps, MapsHandle};
|
||||||
|
use self::position::{resolve_address, Position};
|
||||||
|
|
||||||
|
pub(crate) mod forecast;
|
||||||
|
pub(crate) mod maps;
|
||||||
|
pub(crate) mod position;
|
||||||
|
pub(crate) mod providers;
|
||||||
|
|
||||||
|
/// Handler for retrieving the forecast for an address.
|
||||||
|
#[get("/forecast?<address>&<metrics>")]
|
||||||
|
async fn forecast_address(
|
||||||
|
address: String,
|
||||||
|
metrics: Vec<Metric>,
|
||||||
|
maps_handle: &State<MapsHandle>,
|
||||||
|
) -> Option<Json<Forecast>> {
|
||||||
|
let position = resolve_address(address).await?;
|
||||||
|
let forecast = forecast(position, metrics, maps_handle).await;
|
||||||
|
|
||||||
|
Some(Json(forecast))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handler for retrieving the forecast for a geocoded position.
|
||||||
|
#[get("/forecast?<lat>&<lon>&<metrics>", rank = 2)]
|
||||||
|
async fn forecast_geo(
|
||||||
|
lat: f64,
|
||||||
|
lon: f64,
|
||||||
|
metrics: Vec<Metric>,
|
||||||
|
maps_handle: &State<MapsHandle>,
|
||||||
|
) -> Json<Forecast> {
|
||||||
|
let position = Position::new(lat, lon);
|
||||||
|
let forecast = forecast(position, metrics, maps_handle).await;
|
||||||
|
|
||||||
|
Json(forecast)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handler for showing the current map with the geocoded position of an address for a specific
|
||||||
|
/// metric.
|
||||||
|
///
|
||||||
|
/// Note: This handler is mosly used for debugging purposes!
|
||||||
|
#[get("/map?<address>&<metric>")]
|
||||||
|
async fn map_address(
|
||||||
|
address: String,
|
||||||
|
metric: Metric,
|
||||||
|
maps_handle: &State<MapsHandle>,
|
||||||
|
) -> Option<Custom<Vec<u8>>> {
|
||||||
|
let position = resolve_address(address).await?;
|
||||||
|
let image_data = mark_map(position, metric, maps_handle).await;
|
||||||
|
|
||||||
|
image_data.map(|id| Custom(ContentType::PNG, id))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handler for showing the current map with the geocoded position for a specific metric.
|
||||||
|
///
|
||||||
|
/// Note: This handler is mosly used for debugging purposes!
|
||||||
|
#[get("/map?<lat>&<lon>&<metric>", rank = 2)]
|
||||||
|
async fn map_geo(
|
||||||
|
lat: f64,
|
||||||
|
lon: f64,
|
||||||
|
metric: Metric,
|
||||||
|
maps_handle: &State<MapsHandle>,
|
||||||
|
) -> Option<Custom<Vec<u8>>> {
|
||||||
|
let position = Position::new(lat, lon);
|
||||||
|
let image_data = mark_map(position, metric, maps_handle).await;
|
||||||
|
|
||||||
|
image_data.map(|id| Custom(ContentType::PNG, id))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets up Rocket.
|
||||||
|
fn rocket(maps_handle: MapsHandle) -> Rocket<Build> {
|
||||||
|
rocket::build().manage(maps_handle).mount(
|
||||||
|
"/",
|
||||||
|
routes![forecast_address, forecast_geo, map_address, map_geo],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets up Rocket and the maps cache refresher task.
|
||||||
|
pub fn setup() -> (Rocket<Build>, impl Future<Output = ()>) {
|
||||||
|
let maps = Maps::new();
|
||||||
|
let maps_handle = Arc::new(Mutex::new(maps));
|
||||||
|
let maps_refresher = maps::run(Arc::clone(&maps_handle));
|
||||||
|
let rocket = rocket(maps_handle);
|
||||||
|
|
||||||
|
(rocket, maps_refresher)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use assert_float_eq::*;
|
||||||
|
use assert_matches::assert_matches;
|
||||||
|
use image::{DynamicImage, Rgba, RgbaImage};
|
||||||
|
use rocket::http::Status;
|
||||||
|
use rocket::local::blocking::Client;
|
||||||
|
use rocket::serde::json::Value as JsonValue;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn maps_stub(map_count: u32) -> DynamicImage {
|
||||||
|
let map_color = Rgba::from([73, 218, 33, 255]); // First color from map key.
|
||||||
|
|
||||||
|
DynamicImage::ImageRgba8(RgbaImage::from_pixel(820 * map_count, 988, map_color))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn maps_handle_stub() -> MapsHandle {
|
||||||
|
let mut maps = Maps::new();
|
||||||
|
maps.pollen = Some(maps_stub(24));
|
||||||
|
maps.uvi = Some(maps_stub(5));
|
||||||
|
|
||||||
|
Arc::new(Mutex::new(maps))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn forecast_address() {
|
||||||
|
let maps_handle = maps_handle_stub();
|
||||||
|
let client = Client::tracked(rocket(maps_handle)).expect("Not a valid Rocket instance");
|
||||||
|
|
||||||
|
// Get an empty forecast for the provided address.
|
||||||
|
let response = client.get("/forecast?address=eindhoven").dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
let json = response.into_json::<JsonValue>().expect("Not valid JSON");
|
||||||
|
assert_f64_near!(json["lat"].as_f64().unwrap(), 51.4392648);
|
||||||
|
assert_f64_near!(json["lon"].as_f64().unwrap(), 5.478633);
|
||||||
|
assert_matches!(json["time"], JsonValue::Number(_));
|
||||||
|
assert_matches!(json.get("AQI"), None);
|
||||||
|
assert_matches!(json.get("NO2"), None);
|
||||||
|
assert_matches!(json.get("O3"), None);
|
||||||
|
assert_matches!(json.get("PAQI"), None);
|
||||||
|
assert_matches!(json.get("PM10"), None);
|
||||||
|
assert_matches!(json.get("pollen"), None);
|
||||||
|
assert_matches!(json.get("precipitation"), None);
|
||||||
|
assert_matches!(json.get("UVI"), None);
|
||||||
|
|
||||||
|
// Get a forecast with all metrics for the provided address.
|
||||||
|
let response = client
|
||||||
|
.get("/forecast?address=eindhoven&metrics=all")
|
||||||
|
.dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
let json = response.into_json::<JsonValue>().expect("Not valid JSON");
|
||||||
|
assert_f64_near!(json["lat"].as_f64().unwrap(), 51.4392648);
|
||||||
|
assert_f64_near!(json["lon"].as_f64().unwrap(), 5.478633);
|
||||||
|
assert_matches!(json["time"], JsonValue::Number(_));
|
||||||
|
assert_matches!(json.get("AQI"), Some(JsonValue::Array(_)));
|
||||||
|
assert_matches!(json.get("NO2"), Some(JsonValue::Array(_)));
|
||||||
|
assert_matches!(json.get("O3"), Some(JsonValue::Array(_)));
|
||||||
|
assert_matches!(json.get("PAQI"), Some(JsonValue::Array(_)));
|
||||||
|
assert_matches!(json.get("PM10"), Some(JsonValue::Array(_)));
|
||||||
|
assert_matches!(json.get("pollen"), Some(JsonValue::Array(_)));
|
||||||
|
assert_matches!(json.get("precipitation"), Some(JsonValue::Array(_)));
|
||||||
|
assert_matches!(json.get("UVI"), Some(JsonValue::Array(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn forecast_geo() {
|
||||||
|
let maps_handle = maps_handle_stub();
|
||||||
|
let client = Client::tracked(rocket(maps_handle)).expect("valid Rocket instance");
|
||||||
|
|
||||||
|
// Get an empty forecast for the geocoded location.
|
||||||
|
let response = client.get("/forecast?lat=51.4&lon=5.5").dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
let json = response.into_json::<JsonValue>().expect("Not valid JSON");
|
||||||
|
assert_f64_near!(json["lat"].as_f64().unwrap(), 51.4);
|
||||||
|
assert_f64_near!(json["lon"].as_f64().unwrap(), 5.5);
|
||||||
|
assert_matches!(json["time"], JsonValue::Number(_));
|
||||||
|
assert_matches!(json.get("AQI"), None);
|
||||||
|
assert_matches!(json.get("NO2"), None);
|
||||||
|
assert_matches!(json.get("O3"), None);
|
||||||
|
assert_matches!(json.get("PAQI"), None);
|
||||||
|
assert_matches!(json.get("PM10"), None);
|
||||||
|
assert_matches!(json.get("pollen"), None);
|
||||||
|
assert_matches!(json.get("precipitation"), None);
|
||||||
|
assert_matches!(json.get("UVI"), None);
|
||||||
|
|
||||||
|
// Get a forecast with all metrics for the geocoded location.
|
||||||
|
let response = client
|
||||||
|
.get("/forecast?lat=51.4&lon=5.5&metrics=all")
|
||||||
|
.dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
let json = response.into_json::<JsonValue>().expect("Not valid JSON");
|
||||||
|
assert_f64_near!(json["lat"].as_f64().unwrap(), 51.4);
|
||||||
|
assert_f64_near!(json["lon"].as_f64().unwrap(), 5.5);
|
||||||
|
assert_matches!(json["time"], JsonValue::Number(_));
|
||||||
|
assert_matches!(json.get("AQI"), Some(JsonValue::Array(_)));
|
||||||
|
assert_matches!(json.get("NO2"), Some(JsonValue::Array(_)));
|
||||||
|
assert_matches!(json.get("O3"), Some(JsonValue::Array(_)));
|
||||||
|
assert_matches!(json.get("PAQI"), Some(JsonValue::Array(_)));
|
||||||
|
assert_matches!(json.get("PM10"), Some(JsonValue::Array(_)));
|
||||||
|
assert_matches!(json.get("pollen"), Some(JsonValue::Array(_)));
|
||||||
|
assert_matches!(json.get("precipitation"), Some(JsonValue::Array(_)));
|
||||||
|
assert_matches!(json.get("UVI"), Some(JsonValue::Array(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn map_address() {
|
||||||
|
let maps_handle = Arc::new(Mutex::new(Maps::new()));
|
||||||
|
let maps_handle_clone = Arc::clone(&maps_handle);
|
||||||
|
let client = Client::tracked(rocket(maps_handle)).expect("Not a valid Rocket instance");
|
||||||
|
|
||||||
|
// No maps available yet.
|
||||||
|
let response = client
|
||||||
|
.get("/map?address=eindhoven&metric=pollen")
|
||||||
|
.dispatch();
|
||||||
|
assert_eq!(response.status(), Status::NotFound);
|
||||||
|
|
||||||
|
// Load some dummy map.
|
||||||
|
let mut maps = maps_handle_clone
|
||||||
|
.lock()
|
||||||
|
.expect("Maps handle mutex was poisoned");
|
||||||
|
maps.pollen = Some(maps_stub(24));
|
||||||
|
drop(maps);
|
||||||
|
|
||||||
|
// There should be a map now.
|
||||||
|
let response = client
|
||||||
|
.get("/map?address=eindhoven&metric=pollen")
|
||||||
|
.dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
assert_eq!(response.content_type(), Some(ContentType::PNG));
|
||||||
|
|
||||||
|
// ... but not if it is out of bounds.
|
||||||
|
let response = client.get("/map?address=berlin&metric=pollen").dispatch();
|
||||||
|
assert_eq!(response.status(), Status::NotFound);
|
||||||
|
|
||||||
|
// No metric selected, don't know which map to show?
|
||||||
|
let response = client.get("/map?address=eindhoven").dispatch();
|
||||||
|
assert_eq!(response.status(), Status::NotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn map_geo() {
|
||||||
|
let maps_handle = Arc::new(Mutex::new(Maps::new()));
|
||||||
|
let maps_handle_clone = Arc::clone(&maps_handle);
|
||||||
|
let client = Client::tracked(rocket(maps_handle)).expect("Not a valid Rocket instance");
|
||||||
|
|
||||||
|
// No metric passed, don't know which map to show?
|
||||||
|
let response = client.get("/map?lat=51.4&lon=5.5").dispatch();
|
||||||
|
assert_eq!(response.status(), Status::NotFound);
|
||||||
|
|
||||||
|
// No maps available yet.
|
||||||
|
let response = client.get("/map?lat=51.4&lon=5.5&metric=pollen").dispatch();
|
||||||
|
assert_eq!(response.status(), Status::NotFound);
|
||||||
|
|
||||||
|
// Load some dummy map.
|
||||||
|
let mut maps = maps_handle_clone
|
||||||
|
.lock()
|
||||||
|
.expect("Maps handle mutex was poisoned");
|
||||||
|
maps.pollen = Some(maps_stub(24));
|
||||||
|
drop(maps);
|
||||||
|
|
||||||
|
// There should be a map now.
|
||||||
|
let response = client.get("/map?lat=51.4&lon=5.5&metric=pollen").dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
assert_eq!(response.content_type(), Some(ContentType::PNG));
|
||||||
|
|
||||||
|
// No metric passed, don't know which map to show?
|
||||||
|
let response = client.get("/map?lat=51.4&lon=5.5").dispatch();
|
||||||
|
assert_eq!(response.status(), Status::NotFound);
|
||||||
|
}
|
||||||
|
}
|
269
src/main.rs
269
src/main.rs
|
@ -7,111 +7,24 @@
|
||||||
)]
|
)]
|
||||||
#![deny(missing_docs)]
|
#![deny(missing_docs)]
|
||||||
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use rocket::http::ContentType;
|
|
||||||
use rocket::response::content::Custom;
|
|
||||||
use rocket::serde::json::Json;
|
|
||||||
use rocket::tokio::{self, select};
|
use rocket::tokio::{self, select};
|
||||||
use rocket::{get, routes, Build, Rocket, State};
|
|
||||||
|
|
||||||
pub(crate) use self::forecast::Metric;
|
/// Starts the main maps refresh task and sets up and launches Rocket.
|
||||||
use self::forecast::{forecast, Forecast};
|
|
||||||
pub(crate) use self::maps::{mark_map, Maps, MapsHandle};
|
|
||||||
use self::position::{resolve_address, Position};
|
|
||||||
|
|
||||||
pub(crate) mod forecast;
|
|
||||||
pub(crate) mod maps;
|
|
||||||
pub(crate) mod position;
|
|
||||||
pub(crate) mod providers;
|
|
||||||
|
|
||||||
/// Handler for retrieving the forecast for an address.
|
|
||||||
#[get("/forecast?<address>&<metrics>")]
|
|
||||||
async fn forecast_address(
|
|
||||||
address: String,
|
|
||||||
metrics: Vec<Metric>,
|
|
||||||
maps_handle: &State<MapsHandle>,
|
|
||||||
) -> Option<Json<Forecast>> {
|
|
||||||
let position = resolve_address(address).await?;
|
|
||||||
let forecast = forecast(position, metrics, maps_handle).await;
|
|
||||||
|
|
||||||
Some(Json(forecast))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handler for retrieving the forecast for a geocoded position.
|
|
||||||
#[get("/forecast?<lat>&<lon>&<metrics>", rank = 2)]
|
|
||||||
async fn forecast_geo(
|
|
||||||
lat: f64,
|
|
||||||
lon: f64,
|
|
||||||
metrics: Vec<Metric>,
|
|
||||||
maps_handle: &State<MapsHandle>,
|
|
||||||
) -> Json<Forecast> {
|
|
||||||
let position = Position::new(lat, lon);
|
|
||||||
let forecast = forecast(position, metrics, maps_handle).await;
|
|
||||||
|
|
||||||
Json(forecast)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handler for showing the current map with the geocoded position of an address for a specific
|
|
||||||
/// metric.
|
|
||||||
///
|
|
||||||
/// Note: This handler is mosly used for debugging purposes!
|
|
||||||
#[get("/map?<address>&<metric>")]
|
|
||||||
async fn map_address(
|
|
||||||
address: String,
|
|
||||||
metric: Metric,
|
|
||||||
maps_handle: &State<MapsHandle>,
|
|
||||||
) -> Option<Custom<Vec<u8>>> {
|
|
||||||
let position = resolve_address(address).await?;
|
|
||||||
let image_data = mark_map(position, metric, maps_handle).await;
|
|
||||||
|
|
||||||
image_data.map(|id| Custom(ContentType::PNG, id))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handler for showing the current map with the geocoded position for a specific metric.
|
|
||||||
///
|
|
||||||
/// Note: This handler is mosly used for debugging purposes!
|
|
||||||
#[get("/map?<lat>&<lon>&<metric>", rank = 2)]
|
|
||||||
async fn map_geo(
|
|
||||||
lat: f64,
|
|
||||||
lon: f64,
|
|
||||||
metric: Metric,
|
|
||||||
maps_handle: &State<MapsHandle>,
|
|
||||||
) -> Option<Custom<Vec<u8>>> {
|
|
||||||
let position = Position::new(lat, lon);
|
|
||||||
let image_data = mark_map(position, metric, maps_handle).await;
|
|
||||||
|
|
||||||
image_data.map(|id| Custom(ContentType::PNG, id))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets up Rocket.
|
|
||||||
fn rocket(maps_handle: MapsHandle) -> Rocket<Build> {
|
|
||||||
rocket::build().manage(maps_handle).mount(
|
|
||||||
"/",
|
|
||||||
routes![forecast_address, forecast_geo, map_address, map_geo],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Starts the main maps refresh loop and sets up and launches Rocket.
|
|
||||||
///
|
|
||||||
/// See [`maps::run`] for the maps refresh loop.
|
|
||||||
#[rocket::main]
|
#[rocket::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
color_eyre::install()?;
|
color_eyre::install()?;
|
||||||
|
|
||||||
let maps = Maps::new();
|
let (rocket, maps_refresher) = sinoptik::setup();
|
||||||
let maps_handle = Arc::new(Mutex::new(maps));
|
let rocket = rocket.ignite().await?;
|
||||||
let maps_updater = tokio::spawn(maps::run(Arc::clone(&maps_handle)));
|
|
||||||
|
|
||||||
let rocket = rocket(maps_handle).ignite().await?;
|
|
||||||
let shutdown = rocket.shutdown();
|
let shutdown = rocket.shutdown();
|
||||||
|
let maps_refresher = tokio::spawn(maps_refresher);
|
||||||
|
|
||||||
select! {
|
select! {
|
||||||
result = rocket.launch() => {
|
result = rocket.launch() => {
|
||||||
result?
|
result?
|
||||||
}
|
}
|
||||||
result = maps_updater => {
|
result = maps_refresher => {
|
||||||
shutdown.notify();
|
shutdown.notify();
|
||||||
result?
|
result?
|
||||||
}
|
}
|
||||||
|
@ -119,175 +32,3 @@ async fn main() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use assert_float_eq::*;
|
|
||||||
use assert_matches::assert_matches;
|
|
||||||
use image::{DynamicImage, Rgba, RgbaImage};
|
|
||||||
use rocket::http::Status;
|
|
||||||
use rocket::local::blocking::Client;
|
|
||||||
use rocket::serde::json::Value as JsonValue;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
fn maps_stub(map_count: u32) -> DynamicImage {
|
|
||||||
let map_color = Rgba::from([73, 218, 33, 255]); // First color from map key.
|
|
||||||
|
|
||||||
DynamicImage::ImageRgba8(RgbaImage::from_pixel(820 * map_count, 988, map_color))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn maps_handle_stub() -> MapsHandle {
|
|
||||||
let mut maps = Maps::new();
|
|
||||||
maps.pollen = Some(maps_stub(24));
|
|
||||||
maps.uvi = Some(maps_stub(5));
|
|
||||||
|
|
||||||
Arc::new(Mutex::new(maps))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn forecast_address() {
|
|
||||||
let maps_handle = maps_handle_stub();
|
|
||||||
let client = Client::tracked(rocket(maps_handle)).expect("Not a valid Rocket instance");
|
|
||||||
|
|
||||||
// Get an empty forecast for the provided address.
|
|
||||||
let response = client.get("/forecast?address=eindhoven").dispatch();
|
|
||||||
assert_eq!(response.status(), Status::Ok);
|
|
||||||
let json = response.into_json::<JsonValue>().expect("Not valid JSON");
|
|
||||||
assert_f64_near!(json["lat"].as_f64().unwrap(), 51.4392648);
|
|
||||||
assert_f64_near!(json["lon"].as_f64().unwrap(), 5.478633);
|
|
||||||
assert_matches!(json["time"], JsonValue::Number(_));
|
|
||||||
assert_matches!(json.get("AQI"), None);
|
|
||||||
assert_matches!(json.get("NO2"), None);
|
|
||||||
assert_matches!(json.get("O3"), None);
|
|
||||||
assert_matches!(json.get("PAQI"), None);
|
|
||||||
assert_matches!(json.get("PM10"), None);
|
|
||||||
assert_matches!(json.get("pollen"), None);
|
|
||||||
assert_matches!(json.get("precipitation"), None);
|
|
||||||
assert_matches!(json.get("UVI"), None);
|
|
||||||
|
|
||||||
// Get a forecast with all metrics for the provided address.
|
|
||||||
let response = client
|
|
||||||
.get("/forecast?address=eindhoven&metrics=all")
|
|
||||||
.dispatch();
|
|
||||||
assert_eq!(response.status(), Status::Ok);
|
|
||||||
let json = response.into_json::<JsonValue>().expect("Not valid JSON");
|
|
||||||
assert_f64_near!(json["lat"].as_f64().unwrap(), 51.4392648);
|
|
||||||
assert_f64_near!(json["lon"].as_f64().unwrap(), 5.478633);
|
|
||||||
assert_matches!(json["time"], JsonValue::Number(_));
|
|
||||||
assert_matches!(json.get("AQI"), Some(JsonValue::Array(_)));
|
|
||||||
assert_matches!(json.get("NO2"), Some(JsonValue::Array(_)));
|
|
||||||
assert_matches!(json.get("O3"), Some(JsonValue::Array(_)));
|
|
||||||
assert_matches!(json.get("PAQI"), Some(JsonValue::Array(_)));
|
|
||||||
assert_matches!(json.get("PM10"), Some(JsonValue::Array(_)));
|
|
||||||
assert_matches!(json.get("pollen"), Some(JsonValue::Array(_)));
|
|
||||||
assert_matches!(json.get("precipitation"), Some(JsonValue::Array(_)));
|
|
||||||
assert_matches!(json.get("UVI"), Some(JsonValue::Array(_)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn forecast_geo() {
|
|
||||||
let maps_handle = maps_handle_stub();
|
|
||||||
let client = Client::tracked(rocket(maps_handle)).expect("valid Rocket instance");
|
|
||||||
|
|
||||||
// Get an empty forecast for the geocoded location.
|
|
||||||
let response = client.get("/forecast?lat=51.4&lon=5.5").dispatch();
|
|
||||||
assert_eq!(response.status(), Status::Ok);
|
|
||||||
let json = response.into_json::<JsonValue>().expect("Not valid JSON");
|
|
||||||
assert_f64_near!(json["lat"].as_f64().unwrap(), 51.4);
|
|
||||||
assert_f64_near!(json["lon"].as_f64().unwrap(), 5.5);
|
|
||||||
assert_matches!(json["time"], JsonValue::Number(_));
|
|
||||||
assert_matches!(json.get("AQI"), None);
|
|
||||||
assert_matches!(json.get("NO2"), None);
|
|
||||||
assert_matches!(json.get("O3"), None);
|
|
||||||
assert_matches!(json.get("PAQI"), None);
|
|
||||||
assert_matches!(json.get("PM10"), None);
|
|
||||||
assert_matches!(json.get("pollen"), None);
|
|
||||||
assert_matches!(json.get("precipitation"), None);
|
|
||||||
assert_matches!(json.get("UVI"), None);
|
|
||||||
|
|
||||||
// Get a forecast with all metrics for the geocoded location.
|
|
||||||
let response = client
|
|
||||||
.get("/forecast?lat=51.4&lon=5.5&metrics=all")
|
|
||||||
.dispatch();
|
|
||||||
assert_eq!(response.status(), Status::Ok);
|
|
||||||
let json = response.into_json::<JsonValue>().expect("Not valid JSON");
|
|
||||||
assert_f64_near!(json["lat"].as_f64().unwrap(), 51.4);
|
|
||||||
assert_f64_near!(json["lon"].as_f64().unwrap(), 5.5);
|
|
||||||
assert_matches!(json["time"], JsonValue::Number(_));
|
|
||||||
assert_matches!(json.get("AQI"), Some(JsonValue::Array(_)));
|
|
||||||
assert_matches!(json.get("NO2"), Some(JsonValue::Array(_)));
|
|
||||||
assert_matches!(json.get("O3"), Some(JsonValue::Array(_)));
|
|
||||||
assert_matches!(json.get("PAQI"), Some(JsonValue::Array(_)));
|
|
||||||
assert_matches!(json.get("PM10"), Some(JsonValue::Array(_)));
|
|
||||||
assert_matches!(json.get("pollen"), Some(JsonValue::Array(_)));
|
|
||||||
assert_matches!(json.get("precipitation"), Some(JsonValue::Array(_)));
|
|
||||||
assert_matches!(json.get("UVI"), Some(JsonValue::Array(_)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn map_address() {
|
|
||||||
let maps_handle = Arc::new(Mutex::new(Maps::new()));
|
|
||||||
let maps_handle_clone = Arc::clone(&maps_handle);
|
|
||||||
let client = Client::tracked(rocket(maps_handle)).expect("Not a valid Rocket instance");
|
|
||||||
|
|
||||||
// No maps available yet.
|
|
||||||
let response = client
|
|
||||||
.get("/map?address=eindhoven&metric=pollen")
|
|
||||||
.dispatch();
|
|
||||||
assert_eq!(response.status(), Status::NotFound);
|
|
||||||
|
|
||||||
// Load some dummy map.
|
|
||||||
let mut maps = maps_handle_clone
|
|
||||||
.lock()
|
|
||||||
.expect("Maps handle mutex was poisoned");
|
|
||||||
maps.pollen = Some(maps_stub(24));
|
|
||||||
drop(maps);
|
|
||||||
|
|
||||||
// There should be a map now.
|
|
||||||
let response = client
|
|
||||||
.get("/map?address=eindhoven&metric=pollen")
|
|
||||||
.dispatch();
|
|
||||||
assert_eq!(response.status(), Status::Ok);
|
|
||||||
assert_eq!(response.content_type(), Some(ContentType::PNG));
|
|
||||||
|
|
||||||
// ... but not if it is out of bounds.
|
|
||||||
let response = client.get("/map?address=berlin&metric=pollen").dispatch();
|
|
||||||
assert_eq!(response.status(), Status::NotFound);
|
|
||||||
|
|
||||||
// No metric selected, don't know which map to show?
|
|
||||||
let response = client.get("/map?address=eindhoven").dispatch();
|
|
||||||
assert_eq!(response.status(), Status::NotFound);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn map_geo() {
|
|
||||||
let maps_handle = Arc::new(Mutex::new(Maps::new()));
|
|
||||||
let maps_handle_clone = Arc::clone(&maps_handle);
|
|
||||||
let client = Client::tracked(rocket(maps_handle)).expect("Not a valid Rocket instance");
|
|
||||||
|
|
||||||
// No metric passed, don't know which map to show?
|
|
||||||
let response = client.get("/map?lat=51.4&lon=5.5").dispatch();
|
|
||||||
assert_eq!(response.status(), Status::NotFound);
|
|
||||||
|
|
||||||
// No maps available yet.
|
|
||||||
let response = client.get("/map?lat=51.4&lon=5.5&metric=pollen").dispatch();
|
|
||||||
assert_eq!(response.status(), Status::NotFound);
|
|
||||||
|
|
||||||
// Load some dummy map.
|
|
||||||
let mut maps = maps_handle_clone
|
|
||||||
.lock()
|
|
||||||
.expect("Maps handle mutex was poisoned");
|
|
||||||
maps.pollen = Some(maps_stub(24));
|
|
||||||
drop(maps);
|
|
||||||
|
|
||||||
// There should be a map now.
|
|
||||||
let response = client.get("/map?lat=51.4&lon=5.5&metric=pollen").dispatch();
|
|
||||||
assert_eq!(response.status(), Status::Ok);
|
|
||||||
assert_eq!(response.content_type(), Some(ContentType::PNG));
|
|
||||||
|
|
||||||
// No metric passed, don't know which map to show?
|
|
||||||
let response = client.get("/map?lat=51.4&lon=5.5").dispatch();
|
|
||||||
assert_eq!(response.status(), Status::NotFound);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -536,7 +536,7 @@ pub(crate) async fn mark_map(
|
||||||
///
|
///
|
||||||
/// Use [`MapsRefresh`] trait methods on `maps_handle` to check whether each maps type needs to be
|
/// Use [`MapsRefresh`] trait methods on `maps_handle` to check whether each maps type needs to be
|
||||||
/// refreshed and uses its retrieval function to update it if necessary.
|
/// refreshed and uses its retrieval function to update it if necessary.
|
||||||
pub(crate) async fn run(maps_handle: MapsHandle) -> ! {
|
pub(crate) async fn run(maps_handle: MapsHandle) {
|
||||||
loop {
|
loop {
|
||||||
println!("🕔 Refreshing the maps (if necessary)...");
|
println!("🕔 Refreshing the maps (if necessary)...");
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue