refactor: clean up db stuff; start comments stuff
							parent
							
								
									f980115d45
								
							
						
					
					
						commit
						fed9c01370
					
				
							
								
								
									
										108
									
								
								src/db.rs
								
								
								
								
							
							
						
						
									
										108
									
								
								src/db.rs
								
								
								
								
							| 
						 | 
				
			
			@ -1,108 +0,0 @@
 | 
			
		|||
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)
 | 
			
		||||
        })?,
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn get_plant(pool: &DbPool, id: u32) -> Result<Option<Plant>, DbError> {
 | 
			
		||||
    let conn = pool.get()?;
 | 
			
		||||
 | 
			
		||||
    let mut stmt = conn.prepare("select * from plants where id = $1")?;
 | 
			
		||||
    match stmt.query_row((id,), |row| Plant::from_row(row)) {
 | 
			
		||||
        Ok(plant) => Ok(Some(plant)),
 | 
			
		||||
        Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
 | 
			
		||||
        Err(err) => Err(DbError::Db(err)),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,19 @@
 | 
			
		|||
use r2d2_sqlite::rusqlite::{self, Row};
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize, Deserialize)]
 | 
			
		||||
pub struct Comment {
 | 
			
		||||
    id: i32,
 | 
			
		||||
    plant_id: i32,
 | 
			
		||||
    comment: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Comment {
 | 
			
		||||
    pub fn from_row(row: &Row<'_>) -> Result<Self, rusqlite::Error> {
 | 
			
		||||
        Ok(Self {
 | 
			
		||||
            id: row.get(0)?,
 | 
			
		||||
            plant_id: row.get(1)?,
 | 
			
		||||
            comment: row.get(2)?,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,47 @@
 | 
			
		|||
mod comment;
 | 
			
		||||
mod plant;
 | 
			
		||||
 | 
			
		||||
use r2d2_sqlite::{rusqlite, SqliteConnectionManager};
 | 
			
		||||
 | 
			
		||||
use std::{error::Error, fmt};
 | 
			
		||||
 | 
			
		||||
pub use comment::Comment;
 | 
			
		||||
pub use plant::{NewPlant, Plant};
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,73 @@
 | 
			
		|||
use r2d2_sqlite::rusqlite::{self, Row};
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
 | 
			
		||||
use super::{Comment, DbError, DbPool};
 | 
			
		||||
 | 
			
		||||
#[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)?,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn all(pool: &DbPool) -> Result<Vec<Self>, DbError> {
 | 
			
		||||
        let conn = pool.get()?;
 | 
			
		||||
 | 
			
		||||
        let mut stmt = conn.prepare("select * from plants")?;
 | 
			
		||||
        let plants: Result<Vec<_>, _> = stmt.query_map((), Self::from_row)?.collect();
 | 
			
		||||
 | 
			
		||||
        Ok(plants?)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn with_id(pool: &DbPool, id: i32) -> Result<Option<Self>, DbError> {
 | 
			
		||||
        let conn = pool.get()?;
 | 
			
		||||
 | 
			
		||||
        let mut stmt = conn.prepare("select * from plants where id = $1")?;
 | 
			
		||||
        match stmt.query_row((id,), |row| Plant::from_row(row)) {
 | 
			
		||||
            Ok(plant) => Ok(Some(plant)),
 | 
			
		||||
            Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
 | 
			
		||||
            Err(err) => Err(DbError::Db(err)),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn comments(&self, pool: &DbPool) -> Result<Vec<Comment>, DbError> {
 | 
			
		||||
        let conn = pool.get()?;
 | 
			
		||||
        let mut stmt = conn.prepare("select * from plant_comments where plant_id = $1")?;
 | 
			
		||||
 | 
			
		||||
        let comments: Result<Vec<_>, _> = stmt.query_map((self.id,), Comment::from_row)?.collect();
 | 
			
		||||
        Ok(comments?)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Deserialize)]
 | 
			
		||||
pub struct NewPlant {
 | 
			
		||||
    name: String,
 | 
			
		||||
    species: String,
 | 
			
		||||
    description: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl NewPlant {
 | 
			
		||||
    pub fn insert(self, pool: &DbPool) -> 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(
 | 
			
		||||
            (&self.name, &self.species, &self.description),
 | 
			
		||||
            Plant::from_row,
 | 
			
		||||
        )?)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -7,9 +7,10 @@ use r2d2_sqlite::{rusqlite, SqliteConnectionManager};
 | 
			
		|||
use tera::Tera;
 | 
			
		||||
use tower_http::compression::CompressionLayer;
 | 
			
		||||
 | 
			
		||||
const MIGRATIONS: [&str; 2] = [
 | 
			
		||||
const MIGRATIONS: [&str; 3] = [
 | 
			
		||||
    include_str!("migrations/000_initial.sql"),
 | 
			
		||||
    include_str!("migrations/001_plants.sql"),
 | 
			
		||||
    include_str!("migrations/002_comments.sql"),
 | 
			
		||||
];
 | 
			
		||||
const STATIC_FILES: [(&str, &'static str); 1] = [(
 | 
			
		||||
    "htmx_2.0.4.min.js",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
create table comments (
 | 
			
		||||
    id integer primary key,
 | 
			
		||||
    plant_id integer references plants (id),
 | 
			
		||||
    comment text not null
 | 
			
		||||
);
 | 
			
		||||
| 
						 | 
				
			
			@ -10,7 +10,7 @@ use axum::{
 | 
			
		|||
use tera::Context;
 | 
			
		||||
use tower_http::set_header::SetResponseHeaderLayer;
 | 
			
		||||
 | 
			
		||||
use crate::db;
 | 
			
		||||
use crate::db::Plant;
 | 
			
		||||
 | 
			
		||||
const HX_REQUEST_HEADER: &str = "HX-Request";
 | 
			
		||||
const HX_HISTORY_RESTORE_HEADER: &str = "HX-History-Restore-Request";
 | 
			
		||||
| 
						 | 
				
			
			@ -46,7 +46,7 @@ pub fn app(ctx: crate::Context) -> axum::Router {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
async fn get_index(State(ctx): State<crate::Context>) -> Html<String> {
 | 
			
		||||
    let plants = tokio::task::spawn_blocking(move || db::list_plants(&ctx.pool))
 | 
			
		||||
    let plants = tokio::task::spawn_blocking(move || Plant::all(&ctx.pool))
 | 
			
		||||
        .await
 | 
			
		||||
        .unwrap()
 | 
			
		||||
        .unwrap();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,7 +7,7 @@ use axum::{
 | 
			
		|||
};
 | 
			
		||||
use tera::Context;
 | 
			
		||||
 | 
			
		||||
use crate::db;
 | 
			
		||||
use crate::db::{self, DbError, Plant};
 | 
			
		||||
 | 
			
		||||
use super::render_partial;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -21,15 +21,21 @@ pub fn app(ctx: crate::Context) -> axum::Router {
 | 
			
		|||
async fn get_plant_page(
 | 
			
		||||
    State(ctx): State<crate::Context>,
 | 
			
		||||
    headers: HeaderMap,
 | 
			
		||||
    Path(plant_id): Path<u32>,
 | 
			
		||||
    Path(plant_id): Path<i32>,
 | 
			
		||||
) -> Html<String> {
 | 
			
		||||
    let plant = tokio::task::spawn_blocking(move || db::get_plant(&ctx.pool, plant_id))
 | 
			
		||||
    let (plant, comments) = tokio::task::spawn_blocking(move || {
 | 
			
		||||
        let plant = Plant::with_id(&ctx.pool, plant_id)?.unwrap();
 | 
			
		||||
        let comments = plant.comments(&ctx.pool)?;
 | 
			
		||||
 | 
			
		||||
        Ok::<_, DbError>((plant, comments))
 | 
			
		||||
    })
 | 
			
		||||
    .await
 | 
			
		||||
    .unwrap()
 | 
			
		||||
    .unwrap();
 | 
			
		||||
 | 
			
		||||
    let mut context = Context::new();
 | 
			
		||||
    context.insert("plant", &plant);
 | 
			
		||||
    context.insert("comments", &comments);
 | 
			
		||||
 | 
			
		||||
    let tmpl = if render_partial(&headers) {
 | 
			
		||||
        "partials/plant_info.html"
 | 
			
		||||
| 
						 | 
				
			
			@ -44,7 +50,7 @@ async fn post_plant(
 | 
			
		|||
    State(ctx): State<crate::Context>,
 | 
			
		||||
    Form(plant): Form<db::NewPlant>,
 | 
			
		||||
) -> Html<String> {
 | 
			
		||||
    let plant = tokio::task::spawn_blocking(move || db::insert_plant(&ctx.pool, &plant))
 | 
			
		||||
    let plant = tokio::task::spawn_blocking(move || plant.insert(&ctx.pool))
 | 
			
		||||
        .await
 | 
			
		||||
        .unwrap()
 | 
			
		||||
        .unwrap();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,3 +4,16 @@
 | 
			
		|||
<p>{{ plant.species }}</p>
 | 
			
		||||
<h3>Description</h3>
 | 
			
		||||
<p>{{ plant.description }}</p>
 | 
			
		||||
<h3>Comments</h3>
 | 
			
		||||
<ul id="comments">
 | 
			
		||||
{% for comment in comments %}
 | 
			
		||||
    <li>
 | 
			
		||||
        {{ comment.comment }}
 | 
			
		||||
    </li>
 | 
			
		||||
{% endfor %}
 | 
			
		||||
</ul>
 | 
			
		||||
<form hx-post="/plants/{{ plant.id }}/comments" hx-target="#comments" hx-swap="beforeend">
 | 
			
		||||
    <label for="comment">Comment:</label>
 | 
			
		||||
    <textarea id="comment" name="comment" rows=4></textarea></br>
 | 
			
		||||
    <input type="submit">
 | 
			
		||||
</form>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue