feat: add typed view templates
parent
9ec0e6f179
commit
4e104d533c
|
@ -1,6 +1,7 @@
|
||||||
mod cli;
|
mod cli;
|
||||||
mod db;
|
mod db;
|
||||||
mod server;
|
mod server;
|
||||||
|
mod template;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
fs,
|
fs,
|
||||||
|
|
|
@ -20,6 +20,7 @@ use std::{io::BufWriter, path::PathBuf};
|
||||||
use super::error::AppError;
|
use super::error::AppError;
|
||||||
use crate::{
|
use crate::{
|
||||||
db::{self, Image, NewImage, Pagination},
|
db::{self, Image, NewImage, Pagination},
|
||||||
|
template::{Template, View},
|
||||||
IMG_DIR,
|
IMG_DIR,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -77,12 +78,9 @@ async fn get_images(
|
||||||
let is_final_page = images.len() < page.per_page.try_into().unwrap();
|
let is_final_page = images.len() < page.per_page.try_into().unwrap();
|
||||||
context.insert("is_final_page", &is_final_page);
|
context.insert("is_final_page", &is_final_page);
|
||||||
|
|
||||||
Ok(Html(super::render_view(
|
Ok(Html(
|
||||||
&ctx.tera,
|
View::Images.headers(&headers).render(&ctx.tera, &context)?,
|
||||||
"views/images.html",
|
))
|
||||||
&context,
|
|
||||||
&headers,
|
|
||||||
)?))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_image_original(
|
async fn get_image_original(
|
||||||
|
|
|
@ -18,40 +18,14 @@ use axum::{
|
||||||
use tera::Context;
|
use tera::Context;
|
||||||
use tower_http::{services::ServeDir, set_header::SetResponseHeaderLayer};
|
use tower_http::{services::ServeDir, set_header::SetResponseHeaderLayer};
|
||||||
|
|
||||||
use crate::db::Plant;
|
use crate::{
|
||||||
|
db::Plant,
|
||||||
|
template::{Template, View},
|
||||||
|
};
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, error::AppError>;
|
pub type Result<T> = std::result::Result<T, error::AppError>;
|
||||||
|
|
||||||
const HX_REQUEST_HEADER: &str = "HX-Request";
|
const HX_REQUEST_HEADER: &str = "HX-Request";
|
||||||
const HX_HISTORY_RESTORE_HEADER: &str = "HX-History-Restore-Request";
|
|
||||||
|
|
||||||
pub fn should_render_full(headers: &HeaderMap) -> bool {
|
|
||||||
let is_htmx_req = headers.get(HX_REQUEST_HEADER).is_some();
|
|
||||||
let is_hist_restore_req = headers
|
|
||||||
.get(HX_HISTORY_RESTORE_HEADER)
|
|
||||||
.map(|val| val == HeaderValue::from_static("true"))
|
|
||||||
.unwrap_or(false);
|
|
||||||
|
|
||||||
!is_htmx_req || is_hist_restore_req
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn render_view(
|
|
||||||
tera: &tera::Tera,
|
|
||||||
view: &str,
|
|
||||||
ctx: &tera::Context,
|
|
||||||
headers: &HeaderMap,
|
|
||||||
) -> tera::Result<String> {
|
|
||||||
let view = tera.render(view, ctx)?;
|
|
||||||
|
|
||||||
if should_render_full(headers) {
|
|
||||||
let mut ctx = tera::Context::new();
|
|
||||||
ctx.insert("view", &view);
|
|
||||||
|
|
||||||
tera.render("base.html", &ctx)
|
|
||||||
} else {
|
|
||||||
Ok(view)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn app(ctx: crate::Context, static_dir: impl AsRef<Path>) -> axum::Router {
|
pub fn app(ctx: crate::Context, static_dir: impl AsRef<Path>) -> axum::Router {
|
||||||
let router = Router::new()
|
let router = Router::new()
|
||||||
|
@ -84,23 +58,18 @@ pub async fn render_home(ctx: crate::Context, headers: &HeaderMap) -> Result<Htm
|
||||||
|
|
||||||
let mut context = Context::new();
|
let mut context = Context::new();
|
||||||
context.insert("plants", &plants);
|
context.insert("plants", &plants);
|
||||||
Ok(Html(render_view(
|
|
||||||
&ctx.tera,
|
Ok(Html(
|
||||||
"views/index.html",
|
View::Index.headers(headers).render(&ctx.tera, &context)?,
|
||||||
&context,
|
))
|
||||||
headers,
|
|
||||||
)?))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_login(ctx: crate::Context, headers: &HeaderMap) -> Result<Html<String>> {
|
pub fn render_login(ctx: crate::Context, headers: &HeaderMap) -> Result<Html<String>> {
|
||||||
let context = Context::new();
|
let context = Context::new();
|
||||||
|
|
||||||
Ok(Html(render_view(
|
Ok(Html(
|
||||||
&ctx.tera,
|
View::Login.headers(headers).render(&ctx.tera, &context)?,
|
||||||
"views/login.html",
|
))
|
||||||
&context,
|
|
||||||
&headers,
|
|
||||||
)?))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_index(State(ctx): State<crate::Context>, headers: HeaderMap) -> Result<Html<String>> {
|
async fn get_index(State(ctx): State<crate::Context>, headers: HeaderMap) -> Result<Html<String>> {
|
||||||
|
|
|
@ -7,7 +7,10 @@ use axum::{
|
||||||
};
|
};
|
||||||
use tera::Context;
|
use tera::Context;
|
||||||
|
|
||||||
use crate::db::{self, DbError, Event, Pagination, Plant};
|
use crate::{
|
||||||
|
db::{self, DbError, Event, Pagination, Plant},
|
||||||
|
template::{Template, View},
|
||||||
|
};
|
||||||
|
|
||||||
use super::{error::AppError, query::ToQuery};
|
use super::{error::AppError, query::ToQuery};
|
||||||
|
|
||||||
|
@ -43,12 +46,9 @@ async fn get_plant_page(
|
||||||
context.insert("events", &events);
|
context.insert("events", &events);
|
||||||
context.insert("event_types", &db::EVENT_TYPES);
|
context.insert("event_types", &db::EVENT_TYPES);
|
||||||
|
|
||||||
Ok(Html(super::render_view(
|
Ok(Html(
|
||||||
&ctx.tera,
|
View::Plant.headers(&headers).render(&ctx.tera, &context)?,
|
||||||
"views/plant.html",
|
))
|
||||||
&context,
|
|
||||||
&headers,
|
|
||||||
)?))
|
|
||||||
}
|
}
|
||||||
None => Err(AppError::NotFound),
|
None => Err(AppError::NotFound),
|
||||||
}
|
}
|
||||||
|
@ -75,12 +75,9 @@ async fn get_plants(
|
||||||
context.insert("query", &page.to_query().encode());
|
context.insert("query", &page.to_query().encode());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Html(super::render_view(
|
Ok(Html(
|
||||||
&ctx.tera,
|
View::Plants.headers(&headers).render(&ctx.tera, &context)?,
|
||||||
"views/plants.html",
|
))
|
||||||
&context,
|
|
||||||
&headers,
|
|
||||||
)?))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn post_plant(
|
async fn post_plant(
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
mod view;
|
||||||
|
|
||||||
|
pub use view::View;
|
||||||
|
|
||||||
|
pub trait Template {
|
||||||
|
/// Returns the name or path used to identify the template in the Tera struct
|
||||||
|
fn template(&self) -> &'static str;
|
||||||
|
|
||||||
|
/// Render the template with the given context
|
||||||
|
fn render(&self, tera: &tera::Tera, ctx: &tera::Context) -> tera::Result<String> {
|
||||||
|
tera.render(self.template(), ctx)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
use axum::http::{HeaderMap, HeaderValue};
|
||||||
|
|
||||||
|
use super::Template;
|
||||||
|
|
||||||
|
const HX_REQUEST_HEADER: &str = "HX-Request";
|
||||||
|
const HX_HISTORY_RESTORE_HEADER: &str = "HX-History-Restore-Request";
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub enum View {
|
||||||
|
Plant,
|
||||||
|
Plants,
|
||||||
|
Images,
|
||||||
|
Index,
|
||||||
|
Login,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View {
|
||||||
|
pub fn headers(self, headers: &HeaderMap) -> ViewWrapper {
|
||||||
|
let is_htmx_req = headers.get(HX_REQUEST_HEADER).is_some();
|
||||||
|
let is_hist_restore_req = headers
|
||||||
|
.get(HX_HISTORY_RESTORE_HEADER)
|
||||||
|
.map(|val| val == HeaderValue::from_static("true"))
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
let include_base = !is_htmx_req || is_hist_restore_req;
|
||||||
|
|
||||||
|
ViewWrapper {
|
||||||
|
view: self,
|
||||||
|
include_base,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Template for View {
|
||||||
|
fn template(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
View::Plant => "views/plant.html",
|
||||||
|
View::Plants => "views/plants.html",
|
||||||
|
View::Index => "views/index.html",
|
||||||
|
View::Images => "views/images.html",
|
||||||
|
View::Login => "views/login.html",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ViewWrapper {
|
||||||
|
view: View,
|
||||||
|
include_base: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Template for ViewWrapper {
|
||||||
|
fn template(&self) -> &'static str {
|
||||||
|
self.view.template()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&self, tera: &tera::Tera, ctx: &tera::Context) -> tera::Result<String> {
|
||||||
|
let view = self.view.render(tera, ctx)?;
|
||||||
|
|
||||||
|
if self.include_base {
|
||||||
|
let mut ctx = tera::Context::new();
|
||||||
|
ctx.insert("view", &view);
|
||||||
|
|
||||||
|
tera.render("base.html", &ctx)
|
||||||
|
} else {
|
||||||
|
Ok(view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue