337 lines
12 KiB
Rust
337 lines
12 KiB
Rust
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;
|
|
|
|
use crate::place::Place;
|
|
type Result<T, E = (StatusCode, String)> = std::result::Result<T, E>;
|
|
|
|
fn internal_error<E>(err: E) -> (StatusCode, String)
|
|
where
|
|
E: std::error::Error,
|
|
{
|
|
(StatusCode::INTERNAL_SERVER_ERROR, err.to_string())
|
|
}
|
|
|
|
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"#
|
|
)
|
|
.fetch(&pool)
|
|
.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,
|
|
})
|
|
.try_collect::<Vec<_>>()
|
|
.await
|
|
.map_err(internal_error)?;
|
|
|
|
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"#,
|
|
place.name,
|
|
place.address,
|
|
place.open_hours,
|
|
place.icon,
|
|
place.description,
|
|
place.longitude,
|
|
place.latitude,
|
|
place.url
|
|
)
|
|
.fetch_one(&pool)
|
|
.await
|
|
.map_err(internal_error)?;
|
|
|
|
place.id = Some(id);
|
|
Ok(Json(place))
|
|
}
|
|
|
|
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()))
|
|
}
|
|
}
|
|
|
|
async fn delete_place(State(pool): State<SqlitePool>, Path(id): Path<i64>) -> Result<()> {
|
|
let result = ::sqlx::query!("UPDATE places SET active = FALSE WHERE id = ?", id)
|
|
.execute(&pool)
|
|
.await
|
|
.map_err(internal_error)?;
|
|
|
|
if result.rows_affected() == 1 {
|
|
Ok(())
|
|
} else {
|
|
Err((StatusCode::NOT_FOUND, "".to_owned()))
|
|
}
|
|
}
|
|
|
|
pub fn places_routes(pool: SqlitePool) -> Router {
|
|
Router::new()
|
|
.route("/", get(get_places).put(upsert_place))
|
|
.route("/:id", delete(delete_place))
|
|
.with_state(pool)
|
|
}
|
|
|
|
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());
|
|
}
|
|
}
|