feat: initial page rendering; db stuff

This commit is contained in:
Jef Roosens 2024-12-28 11:44:56 +01:00
parent a410e4f9ec
commit 08f6faef52
No known key found for this signature in database
GPG key ID: 21FD3D77D56BAF49
7 changed files with 692 additions and 23 deletions

View file

@ -1,39 +1,60 @@
mod server;
use std::sync::Arc;
use r2d2_sqlite::{rusqlite, SqliteConnectionManager};
use tera::Tera;
pub type DbPool = r2d2::Pool<SqliteConnectionManager>;
const MIGRATIONS: [&str; 1] = [include_str!("migrations/000_initial.sql")];
const MIGRATIONS: [&str; 2] = [
include_str!("migrations/000_initial.sql"),
include_str!("migrations/001_plants.sql"),
];
#[derive(Clone)]
pub struct Context {
pool: crate::DbPool,
tera: Arc<Tera>,
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let manager = SqliteConnectionManager::file("db.sqlite");
let pool = r2d2::Pool::new(manager).unwrap();
run_migrations(&pool, true).unwrap();
run_migrations(&pool).unwrap();
let tera = load_templates();
let ctx = Context {
pool,
tera: Arc::new(tera),
};
let app = server::app(ctx);
let address = "0.0.0.0:8000";
tracing::info!("Starting server on {address}");
let listener = tokio::net::TcpListener::bind(address).await.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}
fn run_migrations(pool: &DbPool, new: bool) -> rusqlite::Result<()> {
fn run_migrations(pool: &DbPool) -> rusqlite::Result<()> {
let mut conn = pool.get().unwrap();
// Run very first migration
if new {
let tx = conn.transaction()?;
tx.execute(MIGRATIONS[0], ())?;
let cur_time = chrono::Local::now().timestamp();
tx.execute(
"insert into migration_version values ($1, $2)",
[0, cur_time],
)?;
tx.commit()?;
}
let mut next_version = conn.query_row(
"select version from migration_version order by updated_at desc limit 1",
(),
|row| row.get::<_, usize>(0),
)? + 1;
// If the migration version query fails, we assume it's because the table isn't there yet so we
// try to run the first migration
let mut next_version = conn
.query_row(
"select version from migration_version order by updated_at desc limit 1",
(),
|row| row.get::<_, usize>(0).map(|n| n + 1),
)
.unwrap_or(0);
while next_version < MIGRATIONS.len() {
let tx = conn.transaction()?;
@ -48,8 +69,19 @@ fn run_migrations(pool: &DbPool, new: bool) -> rusqlite::Result<()> {
tx.commit()?;
tracing::info!("Applied migration {next_version}");
next_version += 1;
}
Ok(())
}
fn load_templates() -> Tera {
let mut tera = Tera::default();
tera.add_raw_templates(vec![("index.html", include_str!("templates/index.html"))])
.unwrap();
tera
}

View file

@ -0,0 +1,6 @@
create table plants (
id integer primary key,
name text not null,
species text not null,
description text
);

50
src/server/mod.rs Normal file
View file

@ -0,0 +1,50 @@
use axum::{extract::State, response::Html, routing::get, Router};
use r2d2_sqlite::rusqlite::{self, Row};
use serde::Serialize;
use tera::Context;
#[derive(Serialize)]
struct Plant {
id: i32,
name: String,
species: String,
description: Option<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 {
Router::new().route("/", get(get_index)).with_state(ctx)
}
async fn get_index(State(ctx): State<crate::Context>) -> Html<String> {
let plants = tokio::task::spawn_blocking(move || {
let conn = ctx.pool.get().unwrap();
let mut stmt = conn.prepare("select * from plants").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();
context.insert("plants", &plants);
Html(ctx.tera.render("index.html", &context).unwrap())
}

11
src/templates/index.html Normal file
View file

@ -0,0 +1,11 @@
<html>
<body>
<h1>Calathea</h1>
<h2>Plants</h2>
<ul>
{% for plant in plants %}
<li>{{ plant.name }} ({{ plant.species }})</li>
{% endfor %}
</ul>
</body>
</html>