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