Migrate from Rocket to Axum #17

Merged
pitbuster merged 1 commit from axum into main 2023-04-01 23:16:38 -04:00
6 changed files with 643 additions and 954 deletions
Showing only changes of commit fc11177546 - Show all commits

1411
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -8,13 +8,17 @@ links = "sqlite"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rocket = {version = "0.5.0-rc.2", features = ["json"]}
[dependencies.rocket_db_pools]
version = "0.1.0-rc.2"
features = ["sqlx_sqlite"]
axum = "0.6.12"
tracing = "0.1.37"
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
tokio = { version = "1.27.0", features = ["macros", "signal"] }
dotenvy = "0.15.7"
tower-http = { version = "0.4.0", features = ["fs"] }
serde = { version = "1.0.159", features = ["derive"] }
serde_json = "1.0.95"
futures = "0.3.28"
[dependencies.sqlx]
version = "0.5.13"
version = "0.6.3"
default-features = false
features = ["macros", "offline", "migrate", "sqlite"]
features = ["runtime-tokio-native-tls", "macros", "offline", "migrate", "sqlite"]

View file

@ -1,4 +0,0 @@
[default.databases.db]
url = "sqlite://db/huellas.db"
[release]
address = "0.0.0.0"

View file

@ -1,13 +1,51 @@
#[macro_use]
extern crate rocket;
use rocket::fs::FileServer;
use axum::Router;
use sqlx::sqlite::SqlitePool;
use std::net::SocketAddr;
use tower_http::services::ServeDir;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
mod place;
mod routes;
#[launch]
fn rocket() -> _ {
rocket::build()
.mount("/", FileServer::from("static"))
.attach(routes::stage())
#[tokio::main]
async fn main() {
dotenvy::dotenv().unwrap_or_default();
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "huellas=debug".into()),
)
.with(tracing_subscriber::fmt::layer())
.init();
let db_url = dotenvy::var("DATABASE_URL").expect("DATABASE_URL not defined");
let pool = SqlitePool::connect(&db_url)
.await
.expect("can't connect to database");
sqlx::migrate!()
.run(&pool)
.await
.expect("couldn't run migrations");
let app = Router::new()
.nest("/places", routes::places_routes(pool))
.nest_service("/", ServeDir::new("static"));
let port = dotenvy::var("PORT").unwrap_or_default();
let port = str::parse(&port).unwrap_or(3000);
let address = SocketAddr::from(([0, 0, 0, 0], port));
tracing::debug!("listening on {}", address);
axum::Server::bind(&address)
.serve(app.into_make_service())
.with_graceful_shutdown(shutdown_signal())
.await
.unwrap();
}
async fn shutdown_signal() {
tokio::signal::ctrl_c()
.await
.expect("Ctrl-C shutdown signal");
println!("Received shutdown signal");
}

View file

@ -1,7 +1,6 @@
use rocket::serde::{Deserialize, Serialize};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(crate = "rocket::serde")]
pub struct Place {
pub id: Option<i64>,
pub name: String,

View file

@ -1,25 +1,28 @@
use rocket::fairing::{self, AdHoc};
use rocket::response::status::{Accepted, Created, NotFound};
use rocket::serde::json::Json;
use rocket::{Build, Rocket};
use rocket_db_pools::{Connection, Database};
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 rocket::futures::stream::TryStreamExt;
// use rocket::fairing::{self, AdHoc};
use crate::place::Place;
type Result<T, E = rocket::response::Debug<sqlx::Error>> = std::result::Result<T, E>;
type Result<T, E = (StatusCode, String)> = std::result::Result<T, E>;
#[derive(Database)]
#[database("db")]
struct Db(rocket_db_pools::sqlx::SqlitePool);
fn internal_error<E>(err: E) -> (StatusCode, String)
where
E: std::error::Error,
{
(StatusCode::INTERNAL_SERVER_ERROR, err.to_string())
}
#[get("/places")]
async fn get_places(mut db: Connection<Db>) -> Result<Json<Vec<Place>>> {
let places = rocket_db_pools::sqlx::query!(
async fn get_places(State(pool): State<SqlitePool>) -> Result<Json<Vec<Place>>> {
let places = ::sqlx::query!(
"SELECT id, name, address, open_hours, icon, description, url," +
r#"longitude as "longitude: f64", latitude as "latitude: f64" FROM places WHERE active = TRUE"#
)
.fetch(&mut *db)
.fetch(&pool)
.map_ok(|p| Place {
id: Some(p.id),
name: p.name,
@ -32,24 +35,19 @@ async fn get_places(mut db: Connection<Db>) -> Result<Json<Vec<Place>>> {
url: p.url,
})
.try_collect::<Vec<_>>()
.await?;
.await.map_err(internal_error)?;
Ok(Json(places))
}
#[derive(Debug, Responder)]
enum UpsertResponse {
Created(Created<Json<Place>>),
Accepted(Accepted<Json<Place>>),
NotFound(NotFound<Json<Place>>),
}
#[put("/places", format = "json", data = "<place>")]
async fn upsert_place(db: Connection<Db>, place: Json<Place>) -> Result<UpsertResponse> {
async fn upsert_place(
State(pool): State<SqlitePool>,
Json(place): Json<Place>,
) -> Result<Json<Place>> {
if place.id.is_some() {
update_place(db, place).await
update_place(pool, place).await
} else {
insert_place(db, place).await
insert_place(pool, place).await
}
}
@ -57,7 +55,7 @@ struct Id {
id: i64,
}
async fn insert_place(mut db: Connection<Db>, mut place: Json<Place>) -> Result<UpsertResponse> {
async fn insert_place(pool: SqlitePool, mut place: Place) -> Result<Json<Place>> {
let i = ::sqlx::query_as!(
Id,
"INSERT INTO places (name, address, open_hours, icon, description, longitude, latitude, url)\
@ -72,14 +70,15 @@ async fn insert_place(mut db: Connection<Db>, mut place: Json<Place>) -> Result<
place.latitude,
place.url
)
.fetch_one(&mut *db)
.await?;
.fetch_one(&pool)
.await
.map_err(internal_error)?;
place.id = Some(i.id);
Ok(UpsertResponse::Created(Created::new("/places").body(place)))
Ok(Json(place))
}
async fn update_place(mut db: Connection<Db>, place: Json<Place>) -> Result<UpsertResponse> {
async fn update_place(pool: SqlitePool, place: Place) -> Result<Json<Place>> {
let result = ::sqlx::query!(
"UPDATE places SET (name, address, open_hours, icon, description, longitude, latitude, url) = (?, ?, ?, ?, ?, ?, ?, ?) WHERE id = ?",
place.name,
@ -92,43 +91,33 @@ async fn update_place(mut db: Connection<Db>, place: Json<Place>) -> Result<Upse
place.url,
place.id,
)
.execute(&mut *db)
.await?;
.execute(&pool)
.await
.map_err(internal_error)?;
if result.rows_affected() == 1 {
Ok(UpsertResponse::Accepted(Accepted(Some(place))))
Ok(Json(place))
} else {
Ok(UpsertResponse::NotFound(NotFound(place)))
Err((StatusCode::NOT_FOUND, "".to_owned()))
}
}
#[delete("/places/<id>")]
async fn delete_place(mut db: Connection<Db>, id: i64) -> Result<Option<()>> {
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(&mut *db)
.await?;
.execute(&pool)
.await
.map_err(internal_error)?;
Ok((result.rows_affected() == 1).then_some(()))
}
async fn run_migrations(rocket: Rocket<Build>) -> fairing::Result {
match Db::fetch(&rocket) {
Some(db) => match ::sqlx::migrate!("./migrations").run(&**db).await {
Ok(_) => Ok(rocket),
Err(e) => {
error!("Failed to initialize SQLx database: {}", e);
Err(rocket)
}
},
None => Err(rocket),
if result.rows_affected() == 1 {
Ok(())
} else {
Err((StatusCode::NOT_FOUND, "".to_owned()))
}
}
pub fn stage() -> AdHoc {
AdHoc::on_ignite("SQLx Stage", |rocket| async {
rocket
.attach(Db::init())
.attach(AdHoc::try_on_ignite("SQLx Migrations", run_migrations))
.mount("/", routes![upsert_place, get_places, delete_place])
})
pub fn places_routes(pool: SqlitePool) -> Router {
Router::new()
.route("/", get(get_places).put(upsert_place))
.route("/:id", delete(delete_place))
.with_state(pool)
}