diff --git a/src/db.rs b/src/db.rs deleted file mode 100644 index fa1f3cc..0000000 --- a/src/db.rs +++ /dev/null @@ -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; - -#[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 for DbError { - fn from(value: r2d2::Error) -> Self { - Self::Pool(value) - } -} - -impl From 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 { - 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, 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 { - 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, 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)), - } -} diff --git a/src/db/comment.rs b/src/db/comment.rs new file mode 100644 index 0000000..876be31 --- /dev/null +++ b/src/db/comment.rs @@ -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 { + Ok(Self { + id: row.get(0)?, + plant_id: row.get(1)?, + comment: row.get(2)?, + }) + } +} diff --git a/src/db/mod.rs b/src/db/mod.rs new file mode 100644 index 0000000..94a5e99 --- /dev/null +++ b/src/db/mod.rs @@ -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; + +#[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 for DbError { + fn from(value: r2d2::Error) -> Self { + Self::Pool(value) + } +} + +impl From for DbError { + fn from(value: rusqlite::Error) -> Self { + Self::Db(value) + } +} diff --git a/src/db/plant.rs b/src/db/plant.rs new file mode 100644 index 0000000..e7119ae --- /dev/null +++ b/src/db/plant.rs @@ -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 { + Ok(Self { + id: row.get(0)?, + name: row.get(1)?, + species: row.get(2)?, + description: row.get(3)?, + }) + } + + pub fn all(pool: &DbPool) -> Result, DbError> { + let conn = pool.get()?; + + let mut stmt = conn.prepare("select * from plants")?; + let plants: Result, _> = stmt.query_map((), Self::from_row)?.collect(); + + Ok(plants?) + } + + pub fn with_id(pool: &DbPool, id: i32) -> Result, 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, DbError> { + let conn = pool.get()?; + let mut stmt = conn.prepare("select * from plant_comments where plant_id = $1")?; + + let comments: Result, _> = 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 { + 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, + )?) + } +} diff --git a/src/main.rs b/src/main.rs index b09ca2b..8a87b6c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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", diff --git a/src/migrations/002_comments.sql b/src/migrations/002_comments.sql new file mode 100644 index 0000000..1df20f8 --- /dev/null +++ b/src/migrations/002_comments.sql @@ -0,0 +1,5 @@ +create table comments ( + id integer primary key, + plant_id integer references plants (id), + comment text not null +); diff --git a/src/server/mod.rs b/src/server/mod.rs index 84ea21f..dff6395 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -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) -> Html { - 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(); diff --git a/src/server/plants.rs b/src/server/plants.rs index 2dd6c66..a518a78 100644 --- a/src/server/plants.rs +++ b/src/server/plants.rs @@ -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, headers: HeaderMap, - Path(plant_id): Path, + Path(plant_id): Path, ) -> Html { - 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, Form(plant): Form, ) -> Html { - 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(); diff --git a/src/templates/partials/plant_info.html b/src/templates/partials/plant_info.html index 3b1d59a..dd10fab 100644 --- a/src/templates/partials/plant_info.html +++ b/src/templates/partials/plant_info.html @@ -4,3 +4,16 @@

{{ plant.species }}

Description

{{ plant.description }}

+

Comments

+
    +{% for comment in comments %} +
  • + {{ comment.comment }} +
  • +{% endfor %} +
+
+ +
+ +