huellas/src/routes.rs

338 lines
12 KiB
Rust
Raw Normal View History

use axum::extract::{Json, Path, State};
use axum::http::StatusCode;
use axum::routing::{delete, get};
use axum::Router;
use futures::TryStreamExt;
use sqlx::sqlite::SqlitePool;
2022-07-17 17:04:48 -04:00
use crate::place::Place;
type Result<T, E = (StatusCode, String)> = std::result::Result<T, E>;
2022-07-17 17:04:48 -04:00
fn internal_error<E>(err: E) -> (StatusCode, String)
where
E: std::error::Error,
{
(StatusCode::INTERNAL_SERVER_ERROR, err.to_string())
}
2022-07-17 17:04:48 -04:00
async fn get_places(State(pool): State<SqlitePool>) -> Result<Json<Vec<Place>>> {
let places = sqlx::query!(
r#"SELECT id, name, address, open_hours, icon, description, url,
longitude as "longitude: f64", latitude as "latitude: f64"
FROM places
WHERE active = TRUE"#
2022-08-06 22:21:45 -04:00
)
.fetch(&pool)
2022-08-06 22:21:45 -04:00
.map_ok(|p| Place {
id: Some(p.id),
name: p.name,
address: p.address,
open_hours: p.open_hours,
icon: p.icon,
description: p.description,
latitude: p.latitude,
longitude: p.longitude,
url: p.url,
2022-08-06 22:21:45 -04:00
})
.try_collect::<Vec<_>>()
.await
.map_err(internal_error)?;
2022-07-17 17:04:48 -04:00
Ok(Json(places))
}
async fn upsert_place(
State(pool): State<SqlitePool>,
Json(place): Json<Place>,
) -> Result<Json<Place>> {
if place.id.is_some() {
update_place(pool, place).await
} else {
insert_place(pool, place).await
}
}
async fn insert_place(pool: SqlitePool, mut place: Place) -> Result<Json<Place>> {
let id = sqlx::query_scalar!(
r#"INSERT INTO places
(name, address, open_hours, icon, description, longitude, latitude, url)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
RETURNING id"#,
2022-07-17 17:04:48 -04:00
place.name,
place.address,
place.open_hours,
place.icon,
place.description,
place.longitude,
place.latitude,
place.url
2022-07-17 17:04:48 -04:00
)
.fetch_one(&pool)
.await
.map_err(internal_error)?;
2022-07-17 17:04:48 -04:00
place.id = Some(id);
Ok(Json(place))
2022-07-17 17:04:48 -04:00
}
async fn update_place(pool: SqlitePool, place: Place) -> Result<Json<Place>> {
let result = ::sqlx::query!(
r#"UPDATE places
SET (name, address, open_hours, icon, description, longitude, latitude, url)
= (?, ?, ?, ?, ?, ?, ?, ?)
WHERE id = ?"#,
place.name,
place.address,
place.open_hours,
place.icon,
place.description,
place.longitude,
place.latitude,
place.url,
place.id,
)
.execute(&pool)
.await
.map_err(internal_error)?;
if result.rows_affected() == 1 {
Ok(Json(place))
} else {
Err((StatusCode::NOT_FOUND, "".to_owned()))
}
2022-07-17 17:04:48 -04:00
}
async fn delete_place(State(pool): State<SqlitePool>, Path(id): Path<i64>) -> Result<()> {
2022-07-17 17:04:48 -04:00
let result = ::sqlx::query!("UPDATE places SET active = FALSE WHERE id = ?", id)
.execute(&pool)
.await
.map_err(internal_error)?;
2022-07-17 17:04:48 -04:00
if result.rows_affected() == 1 {
Ok(())
} else {
Err((StatusCode::NOT_FOUND, "".to_owned()))
2022-07-17 17:04:48 -04:00
}
}
pub fn places_routes(pool: SqlitePool) -> Router {
Router::new()
.route("/", get(get_places).put(upsert_place))
.route("/:id", delete(delete_place))
.with_state(pool)
2022-07-17 17:04:48 -04:00
}
mod tests {
#![cfg(test)]
use super::places_routes;
use crate::place::Place;
use axum::http::StatusCode;
use axum::Router;
use axum_test_helper::TestClient;
use sqlx::sqlite::SqlitePool;
fn client(pool: &SqlitePool) -> TestClient {
let router = Router::new().nest("/places", places_routes(pool.clone()));
TestClient::new(router)
}
async fn get_from_db(pool: &SqlitePool, id: i64) -> Place {
sqlx::query_as!(
Place,
r#"SELECT id, name, address, open_hours, icon, description, url,
longitude as "longitude: f64", latitude as "latitude: f64"
FROM places
WHERE active = ?"#,
id
)
.fetch_one(pool)
.await
.expect("Couldn't get from DB")
}
#[sqlx::test]
async fn test_add_place(pool: SqlitePool) {
let client = client(&pool);
let mut place = Place {
id: None,
name: "Sherlock Holmes".to_owned(),
address: "221 B Baker Street, London".to_owned(),
description: "Museum and Gift Shop".to_owned(),
icon: "museum".to_owned(),
latitude: 51.5237669,
longitude: -0.1627829,
open_hours: "Tu-Su 09:30-18:00".to_owned(),
url: Some("https://www.sherlock-holmes.co.uk/".to_owned()),
};
// Insert the place
let res = client.put("/places").json(&place).send().await;
// We should get a success on the request
assert_eq!(res.status(), StatusCode::OK);
let res_place: Place = serde_json::from_value(res.json().await).unwrap();
// The inserted place should have an ID
assert!(res_place.id.is_some());
// Add the returned ID to the original place
place.id = res_place.id;
// And now they should be equal
assert_eq!(place, res_place);
// TODO: actually query the DB to check the place was inserted
// confirm that the places stored in the DB are also the same
let db_place = get_from_db(&pool, place.id.unwrap()).await;
assert_eq!(place, db_place);
}
#[sqlx::test]
async fn test_get_places(pool: SqlitePool) {
let client = client(&pool);
let mut places = vec![
Place {
id: None,
name: "Sherlock Holmes".to_owned(),
address: "221 B Baker Street, London".to_owned(),
description: "Museum and Gift Shop".to_owned(),
icon: "museum".to_owned(),
latitude: 51.5237669,
longitude: -0.1627829,
open_hours: "Tu-Su 09:30-18:00".to_owned(),
url: Some("https://www.sherlock-holmes.co.uk/".to_owned()),
},
Place {
id: None,
name: "Museo Nacional de Historia Natural".to_owned(),
address: "Parque Quinta Normal S/N, Santiago".to_owned(),
description: "Museo".to_owned(),
icon: "museum".to_owned(),
latitude: -70.681838888889,
longitude: -33.4421694444449,
open_hours: "Tu-Su 10:00-18:00".to_owned(),
url: Some("https://www.mnhn.gob.cl/".to_owned()),
},
];
// insert the places
for p in &mut places {
let res = client.put("/places").json(&p).send().await;
// We should get a success on the request
assert_eq!(res.status(), StatusCode::OK);
let res_place: Place = serde_json::from_value(res.json().await).unwrap();
// The inserted place should have an ID
assert!(res_place.id.is_some());
// Add the returned ID to the original place
p.id = res_place.id;
}
// and fetch them
let res = client.get("/places").send().await;
// We should get a success on the request
assert_eq!(res.status(), StatusCode::OK);
let mut res_places: Vec<Place> = serde_json::from_value(res.json().await).unwrap();
// and they should be equal
places.sort_by(|a, b| a.id.cmp(&b.id));
res_places.sort_by(|a, b| a.id.cmp(&b.id));
assert_eq!(places, res_places);
}
#[sqlx::test]
async fn test_delete(pool: SqlitePool) {
let client = client(&pool);
let mut places = vec![
Place {
id: None,
name: "Sherlock Holmes".to_owned(),
address: "221 B Baker Street, London".to_owned(),
description: "Museum and Gift Shop".to_owned(),
icon: "museum".to_owned(),
latitude: 51.5237669,
longitude: -0.1627829,
open_hours: "Tu-Su 09:30-18:00".to_owned(),
url: Some("https://www.sherlock-holmes.co.uk/".to_owned()),
},
Place {
id: None,
name: "Museo Nacional de Historia Natural".to_owned(),
address: "Parque Quinta Normal S/N, Santiago".to_owned(),
description: "Museo".to_owned(),
icon: "museum".to_owned(),
latitude: -70.681838888889,
longitude: -33.4421694444449,
open_hours: "Tu-Su 10:00-18:00".to_owned(),
url: Some("https://www.mnhn.gob.cl/".to_owned()),
},
];
// insert the places
for p in &mut places {
let res = client.put("/places").json(&p).send().await;
// We should get a success on the request
assert_eq!(res.status(), StatusCode::OK);
let res_place: Place = serde_json::from_value(res.json().await).unwrap();
// The inserted place should have an ID
assert!(res_place.id.is_some());
// Add the returned ID to the original place
p.id = res_place.id;
}
// delete the first one
let res = client
.delete(&format!("/places/{}", places[0].id.unwrap()))
.send()
.await;
// We should get a success on the request
assert_eq!(res.status(), StatusCode::OK);
// fetch the remaining places
let res = client.get("/places").send().await;
// We should get a success on the request
assert_eq!(res.status(), StatusCode::OK);
let res_places: Vec<Place> = serde_json::from_value(res.json().await).unwrap();
// we should only get the second place
assert_eq!(&places[1..], res_places.as_slice());
}
#[sqlx::test]
async fn test_update(pool: SqlitePool) {
let client = client(&pool);
let mut places = vec![
Place {
id: None,
name: "Sherlock Holmes".to_owned(),
address: "221 B Baker Street, London".to_owned(),
description: "Museum and Gift Shop".to_owned(),
icon: "museum".to_owned(),
latitude: 51.5237669,
longitude: -0.1627829,
open_hours: "Tu-Su 09:30-18:00".to_owned(),
url: Some("https://www.sherlock-holmes.co.uk/".to_owned()),
},
Place {
id: None,
name: "Museo Nacional de Historia Natural".to_owned(),
address: "Parque Quinta Normal S/N, Santiago".to_owned(),
description: "Museo".to_owned(),
icon: "museum".to_owned(),
latitude: -70.681838888889,
longitude: -33.4421694444449,
open_hours: "Tu-Su 10:00-18:00".to_owned(),
url: Some("https://www.mnhn.gob.cl/".to_owned()),
},
];
// insert original place
let res = client.put("/places").json(&places[0]).send().await;
// We should get a success on the request
assert_eq!(res.status(), StatusCode::OK);
let res_place: Place = serde_json::from_value(res.json().await).unwrap();
// The inserted place should have an ID
assert!(res_place.id.is_some());
// Add the returned ID to the new place so we can do the update
places[1].id = res_place.id;
// update the place
let res = client.put("/places").json(&places[1]).send().await;
// We should get a success on the request
assert_eq!(res.status(), StatusCode::OK);
// fetch the places
let res = client.get("/places").send().await;
// We should get a success on the request
assert_eq!(res.status(), StatusCode::OK);
let res_places: Vec<Place> = serde_json::from_value(res.json().await).unwrap();
// we should get the updated place
assert_eq!(&places[1..], res_places.as_slice());
}
}