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)) | ||||
|         .await | ||||
|         .unwrap() | ||||
|         .unwrap(); | ||||
|     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