refactor: move db stuff into own module
parent
412f4cd2c7
commit
f96bf4d193
|
@ -1 +1,2 @@
|
||||||
/target
|
/target
|
||||||
|
db.sqlite
|
||||||
|
|
|
@ -0,0 +1,97 @@
|
||||||
|
use std::{error::Error, fmt};
|
||||||
|
|
||||||
|
use r2d2_sqlite::{
|
||||||
|
rusqlite::{self, Row},
|
||||||
|
SqliteConnectionManager,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub type DbPool = r2d2::Pool<SqliteConnectionManager>;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum DbError {
|
||||||
|
Pool(r2d2::Error),
|
||||||
|
Db(rusqlite::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for DbError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Pool(_) => write!(f, "failed to acquire connection from pool"),
|
||||||
|
Self::Db(_) => write!(f, "error while accessing the database"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for DbError {
|
||||||
|
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||||
|
match self {
|
||||||
|
Self::Pool(err) => Some(err),
|
||||||
|
Self::Db(err) => Some(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<r2d2::Error> for DbError {
|
||||||
|
fn from(value: r2d2::Error) -> Self {
|
||||||
|
Self::Pool(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<rusqlite::Error> for DbError {
|
||||||
|
fn from(value: rusqlite::Error) -> Self {
|
||||||
|
Self::Db(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct Plant {
|
||||||
|
id: i32,
|
||||||
|
name: String,
|
||||||
|
species: String,
|
||||||
|
description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Plant {
|
||||||
|
pub fn from_row(row: &Row<'_>) -> Result<Self, rusqlite::Error> {
|
||||||
|
Ok(Self {
|
||||||
|
id: row.get(0)?,
|
||||||
|
name: row.get(1)?,
|
||||||
|
species: row.get(2)?,
|
||||||
|
description: row.get(3)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct NewPlant {
|
||||||
|
name: String,
|
||||||
|
species: String,
|
||||||
|
description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_plants(pool: &DbPool) -> Result<Vec<Plant>, DbError> {
|
||||||
|
let conn = pool.get()?;
|
||||||
|
let mut stmt = conn.prepare("select * from plants")?;
|
||||||
|
|
||||||
|
let mut plants = Vec::new();
|
||||||
|
|
||||||
|
for plant in stmt.query_map((), |row| Plant::from_row(row))? {
|
||||||
|
plants.push(plant?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(plants)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_plant(pool: &DbPool, plant: &NewPlant) -> Result<Plant, DbError> {
|
||||||
|
let conn = pool.get()?;
|
||||||
|
|
||||||
|
let mut stmt = conn.prepare(
|
||||||
|
"insert into plants (name, species, description) values ($1, $2, $3) returning *",
|
||||||
|
)?;
|
||||||
|
Ok(
|
||||||
|
stmt.query_row((&plant.name, &plant.species, &plant.description), |row| {
|
||||||
|
Plant::from_row(row)
|
||||||
|
})?,
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
mod db;
|
||||||
mod server;
|
mod server;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
@ -6,8 +7,6 @@ use r2d2_sqlite::{rusqlite, SqliteConnectionManager};
|
||||||
use tera::Tera;
|
use tera::Tera;
|
||||||
use tower_http::compression::CompressionLayer;
|
use tower_http::compression::CompressionLayer;
|
||||||
|
|
||||||
pub type DbPool = r2d2::Pool<SqliteConnectionManager>;
|
|
||||||
|
|
||||||
const MIGRATIONS: [&str; 2] = [
|
const MIGRATIONS: [&str; 2] = [
|
||||||
include_str!("migrations/000_initial.sql"),
|
include_str!("migrations/000_initial.sql"),
|
||||||
include_str!("migrations/001_plants.sql"),
|
include_str!("migrations/001_plants.sql"),
|
||||||
|
@ -19,7 +18,7 @@ const STATIC_FILES: [(&str, &'static str); 1] = [(
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Context {
|
pub struct Context {
|
||||||
pool: crate::DbPool,
|
pool: db::DbPool,
|
||||||
tera: Arc<Tera>,
|
tera: Arc<Tera>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,7 +47,7 @@ async fn main() {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_migrations(pool: &DbPool) -> rusqlite::Result<()> {
|
fn run_migrations(pool: &db::DbPool) -> rusqlite::Result<()> {
|
||||||
let mut conn = pool.get().unwrap();
|
let mut conn = pool.get().unwrap();
|
||||||
|
|
||||||
// If the migration version query fails, we assume it's because the table isn't there yet so we
|
// If the migration version query fails, we assume it's because the table isn't there yet so we
|
||||||
|
|
|
@ -4,30 +4,9 @@ use axum::{
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
Form, Router,
|
Form, Router,
|
||||||
};
|
};
|
||||||
use r2d2_sqlite::rusqlite::{self, Row};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use tera::Context;
|
use tera::Context;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
use crate::db;
|
||||||
struct Plant {
|
|
||||||
id: i32,
|
|
||||||
name: String,
|
|
||||||
species: String,
|
|
||||||
description: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<&Row<'_>> for Plant {
|
|
||||||
type Error = rusqlite::Error;
|
|
||||||
|
|
||||||
fn try_from(row: &Row<'_>) -> Result<Self, Self::Error> {
|
|
||||||
Ok(Self {
|
|
||||||
id: row.get(0)?,
|
|
||||||
name: row.get(1)?,
|
|
||||||
species: row.get(2)?,
|
|
||||||
description: row.get(3)?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn app(ctx: crate::Context) -> axum::Router {
|
pub fn app(ctx: crate::Context) -> axum::Router {
|
||||||
let mut router = Router::new()
|
let mut router = Router::new()
|
||||||
|
@ -42,55 +21,24 @@ pub fn app(ctx: crate::Context) -> axum::Router {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_index(State(ctx): State<crate::Context>) -> Html<String> {
|
async fn get_index(State(ctx): State<crate::Context>) -> Html<String> {
|
||||||
let plants = tokio::task::spawn_blocking(move || {
|
let plants = tokio::task::spawn_blocking(move || db::list_plants(&ctx.pool))
|
||||||
let conn = ctx.pool.get().unwrap();
|
.await
|
||||||
|
.unwrap()
|
||||||
let mut stmt = conn.prepare("select * from plants").unwrap();
|
.unwrap();
|
||||||
let mut plants = Vec::new();
|
|
||||||
|
|
||||||
for plant in stmt.query_map((), |row| Plant::try_from(row)).unwrap() {
|
|
||||||
plants.push(plant.unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
plants
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut context = Context::new();
|
let mut context = Context::new();
|
||||||
context.insert("plants", &plants);
|
context.insert("plants", &plants);
|
||||||
Html(ctx.tera.render("index.html", &context).unwrap())
|
Html(ctx.tera.render("index.html", &context).unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct NewPlant {
|
|
||||||
name: String,
|
|
||||||
species: String,
|
|
||||||
description: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn post_plants(
|
async fn post_plants(
|
||||||
State(ctx): State<crate::Context>,
|
State(ctx): State<crate::Context>,
|
||||||
Form(plant): Form<NewPlant>,
|
Form(plant): Form<db::NewPlant>,
|
||||||
) -> Html<String> {
|
) -> Html<String> {
|
||||||
let plant = tokio::task::spawn_blocking(move || {
|
let plant = tokio::task::spawn_blocking(move || db::insert_plant(&ctx.pool, &plant))
|
||||||
let conn = ctx.pool.get().unwrap();
|
.await
|
||||||
|
.unwrap()
|
||||||
let mut stmt = conn
|
.unwrap();
|
||||||
.prepare(
|
|
||||||
"insert into plants (name, species, description) values ($1, $2, $3) returning *",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let plant = stmt
|
|
||||||
.query_row((&plant.name, &plant.species, &plant.description), |row| {
|
|
||||||
Plant::try_from(row)
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
plant
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut context = Context::new();
|
let mut context = Context::new();
|
||||||
context.insert("plant", &plant);
|
context.insert("plant", &plant);
|
||||||
|
|
Loading…
Reference in New Issue