huellas/src/routes.rs
Felipe Contreras Salinas 1bffd10585
feat!: use msgpack instead of json (#40)
Reviewed-on: #40
Co-authored-by: Felipe Contreras Salinas <felipe@bstr.cl>
Co-committed-by: Felipe Contreras Salinas <felipe@bstr.cl>
2024-03-06 21:38:04 -03:00

352 lines
12 KiB
Rust

use axum::extract::{Path, State};
use axum::http::StatusCode;
use axum::routing::{delete, get};
use axum::Router;
use axum_msgpack::MsgPack;
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<MsgPack<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(MsgPack(places))
}
async fn upsert_place(
State(pool): State<SqlitePool>,
MsgPack(place): MsgPack<Place>,
) -> Result<MsgPack<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<MsgPack<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(MsgPack(place))
}
async fn update_place(pool: SqlitePool, place: Place) -> Result<MsgPack<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(MsgPack(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 anyhow::{Context, Result};
use axum::http::StatusCode;
use axum::Router;
use axum_test::TestServer;
use sqlx::sqlite::SqlitePool;
fn server(pool: &SqlitePool) -> Result<TestServer> {
let router = Router::new().nest("/places", places_routes(pool.clone()));
TestServer::new(router)
}
async fn get_from_db(pool: &SqlitePool, id: i64) -> Result<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
.context("Couldn't get from DB")
}
#[sqlx::test]
async fn test_add_place(pool: SqlitePool) -> Result<()> {
let server = server(&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 = server.put("/places").msgpack(&place).await;
// We should get a success on the request
assert_eq!(res.status_code(), StatusCode::OK);
let res_place: Place = res.msgpack();
// 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);
// Check against the place stored in the DB
let db_place = get_from_db(&pool, place.id.unwrap()).await?;
assert_eq!(place, db_place);
Ok(())
}
#[sqlx::test]
async fn test_get_places(pool: SqlitePool) -> Result<()> {
let server = server(&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 = server.put("/places").msgpack(&p).await;
// We should get a success on the request
assert_eq!(res.status_code(), StatusCode::OK);
let res_place: Place = res.msgpack();
// 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 = server.get("/places").await;
// We should get a success on the request
assert_eq!(res.status_code(), StatusCode::OK);
let mut res_places: Vec<Place> = res.msgpack();
// 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);
Ok(())
}
#[sqlx::test]
async fn test_delete(pool: SqlitePool) -> Result<()> {
let server = server(&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 = server.put("/places").msgpack(&p).await;
// We should get a success on the request
assert_eq!(res.status_code(), StatusCode::OK);
let res_place: Place = res.msgpack();
// 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 = server
.delete(&format!("/places/{}", places[0].id.unwrap()))
.await;
// We should get a success on the request
assert_eq!(res.status_code(), StatusCode::OK);
// fetch the remaining places
let res = server.get("/places").await;
// We should get a success on the request
assert_eq!(res.status_code(), StatusCode::OK);
let res_places: Vec<Place> = res.msgpack();
// we should only get the second place
assert_eq!(&places[1..], res_places.as_slice());
Ok(())
}
#[sqlx::test]
async fn test_delete_not_existing(pool: SqlitePool) -> Result<()> {
let server = server(&pool)?;
// Try to delete a non-existing place
let res = server.delete("/places/33").await;
// We should get the corresponding status code
assert_eq!(res.status_code(), StatusCode::NOT_FOUND);
Ok(())
}
#[sqlx::test]
async fn test_update(pool: SqlitePool) -> Result<()> {
let server = server(&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 = server.put("/places").msgpack(&places[0]).await;
// We should get a success on the request
assert_eq!(res.status_code(), StatusCode::OK);
let res_place: Place = res.msgpack();
// 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 = server.put("/places").msgpack(&places[1]).await;
// We should get a success on the request
assert_eq!(res.status_code(), StatusCode::OK);
// fetch the places
let res = server.get("/places").await;
// We should get a success on the request
assert_eq!(res.status_code(), StatusCode::OK);
let res_places: Vec<Place> = res.msgpack();
// we should get the updated place
assert_eq!(&places[1..], res_places.as_slice());
Ok(())
}
}