Refactor template handler module for tighter interfaces

This commit is contained in:
Juno Takano 2026-03-09 09:15:54 -03:00
commit b443116e56
2 changed files with 138 additions and 36 deletions

View file

@ -47,15 +47,17 @@ fn make_body(
context.insert("message", out_message); context.insert("message", out_message);
context.insert("status_code", &out_code.to_string()); context.insert("status_code", &out_code.to_string());
handlers::template::render( match handlers::template::render(
"error", "error",
&context, &context,
Some(&format!( Some(&format!(
"Failed to render template for Error {out_code}: {out_message}" "Failed to render template for Error {out_code}: {out_message}"
)) ))
.cloned(), .cloned(),
) ) {
.0 Ok(rendered) => rendered.html,
Err(error) => error.template.html,
}
} }
pub async fn not_found(State(state): State<GlobalState>) -> Response<Body> { pub async fn not_found(State(state): State<GlobalState>) -> Response<Body> {

View file

@ -31,39 +31,100 @@ pub(in crate::router::handlers) fn with_context(
error_message: Option<String>, error_message: Option<String>,
is_error: bool, is_error: bool,
) -> Response<Body> { ) -> Response<Body> {
let (body, render_status) = render(name, context, error_message); match render(name, context, error_message) {
Ok(rendered) => {
let status_code = if is_error { error_code } else { render_status }; let status_code = if is_error { error_code } else { rendered.code };
make_response(
make_response(&body, status_code, &[(header::CONTENT_TYPE, "text/html")]) &rendered.html,
status_code,
&[(header::CONTENT_TYPE, "text/html")],
)
},
Err(error) => make_response(
&error.template.html,
error.template.code,
&[(header::CONTENT_TYPE, "text/html")],
),
}
} }
/// Renderes a template into a String and error code. #[derive(Debug)]
pub struct Rendered {
pub html: String,
pub code: u16,
}
impl Rendered {
fn ok(html: &str) -> Rendered {
Rendered {
code: 200,
html: String::from(html),
}
}
}
#[derive(Debug)]
pub struct RenderingError {
pub message: String,
pub template: Rendered,
}
impl RenderingError {
fn new(message: &str, code: u16, error: &tera::Error) -> RenderingError {
RenderingError {
message: String::from(message),
template: Rendered {
html: emergency_wrap(error, message),
code,
},
}
}
fn with_template(
message: &str,
code: u16,
template: &str,
) -> RenderingError {
RenderingError {
message: String::from(message),
template: Rendered {
html: String::from(template),
code,
},
}
}
}
impl std::fmt::Display for RenderingError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Rendering Error: {}", self.message)
}
}
/// Renders a template into a String and error code.
/// ///
/// The template name **must not** contain the extension (e.g. `.html`). /// The template name **must not** contain the extension (e.g. `.html`).
pub(in crate::router::handlers) fn render( pub(in crate::router::handlers) fn render(
template: &str, template: &str,
// TODO take Option, skip context if None,
// then template_handler can replace static_template_handler
context: &tera::Context, context: &tera::Context,
error_message: Option<String>, error_message: Option<String>,
) -> (String, u16) { ) -> Result<Rendered, RenderingError> {
let instant = now(); let instant = now();
// TODO just return an Option/String> here
let tera = match tera::Tera::new("./templates/**/*") { let tera = match tera::Tera::new("./templates/**/*") {
Ok(t) => t, Ok(engine) => engine,
Err(e) => { Err(error) => {
return ( return Err(RenderingError::new(
emergency_wrap(&e, "Failed instantiating template engine"), "Failed instantiating template engine",
500, 500,
); &error,
))
}, },
}; };
match tera.render(format!("{template}.html").as_str(), context) { match tera.render(format!("{template}.html").as_str(), context) {
Ok(t) => { Ok(html) => {
tlog!(&instant, "Rendered template {template}"); tlog!(&instant, "Rendered template {template}");
(t, 200) Ok(Rendered::ok(&html))
}, },
Err(e) => { Err(e) => {
let mut error_context = tera::Context::default(); let mut error_context = tera::Context::default();
@ -87,11 +148,21 @@ pub(in crate::router::handlers) fn render(
&StatusCode::INTERNAL_SERVER_ERROR.to_string(), &StatusCode::INTERNAL_SERVER_ERROR.to_string(),
); );
( match tera.render("error.html", &error_context) {
tera.render("error.html", &error_context) Ok(rendered_error) => Err(RenderingError::with_template(
.unwrap_or(out_error_message.clone()), &out_error_message,
500, 500,
) &rendered_error,
)),
Err(error_rendering_error) => Err(RenderingError::new(
&format!(
"Failed to render an error message template for \
\"{out_error_message}\""
),
500,
&error_rendering_error,
)),
}
}, },
} }
} }
@ -192,38 +263,63 @@ mod tests {
context.insert("node", &node); context.insert("node", &node);
context.insert("graph", &graph); context.insert("graph", &graph);
context.insert("incoming", &graph.incoming.get(&node.id)); context.insert("incoming", &graph.incoming.get(&node.id));
let (body, status) = render("node", &context, None); match render("node", &context, None) {
assert_eq!(status, 200); Ok(rendered) => {
assert!(body.matches(payload).count() == 1); assert_eq!(rendered.code, 200);
assert!(rendered.html.matches(payload).count() == 1);
},
Err(error) => {
panic!("Errored on template generation with {error:?}")
},
}
} }
#[test] #[test]
fn render_custom_error_message() { fn render_custom_error_message() {
let payload = "dBgIw8DnNHxJojiXzu445qUC4UpxwZCy"; let payload = "dBgIw8DnNHxJojiXzu445qUC4UpxwZCy";
let (body, status) = render( match render(
"ObH9jYUl4wMhUNcXnuqwVVzHoqx4ufyN", "ObH9jYUl4wMhUNcXnuqwVVzHoqx4ufyN",
&tera::Context::default(), &tera::Context::default(),
Some(payload.to_string()), Some(payload.to_string()),
); ) {
assert_eq!(status, 500); Ok(_) => panic!("Got Ok, expected Error"),
assert!(body.matches(payload).count() == 1); Err(error) => {
assert_eq!(error.template.code, 500);
assert!(error.template.html.matches(payload).count() == 1);
},
}
} }
#[test] #[test]
fn render_empty() { fn render_empty() {
let (body, status) = render( match render(
"R8D1pxwHZDxcH5SMjR7rZEnIzmpkiHkH", "R8D1pxwHZDxcH5SMjR7rZEnIzmpkiHkH",
&tera::Context::default(), &tera::Context::default(),
None, None,
); ) {
assert_eq!(status, 500); Ok(_) => panic!("Got Ok, expected Error"),
assert!(body.matches("Template render failed").count() == 1); Err(error) => {
assert_eq!(error.template.code, 500);
assert!(
error
.template
.html
.matches("Template render failed")
.count()
== 1
);
},
}
} }
#[test] #[test]
fn render_not_found() { fn render_not_found() {
let payload = "OL6kb9qHe7Iwr7wFIRKUTeFhF34BRsQo"; let payload = "OL6kb9qHe7Iwr7wFIRKUTeFhF34BRsQo";
let (body, status) = render(payload, &tera::Context::default(), None); let (body, status) =
match render(payload, &tera::Context::default(), None) {
Ok(_) => panic!("Got Ok, expected Error"),
Err(error) => (error.template.html, error.template.code),
};
assert!(body.matches("TemplateNotFound").count() > 0); assert!(body.matches("TemplateNotFound").count() > 0);
assert!(body.matches(payload).count() > 0); assert!(body.matches(payload).count() > 0);
@ -232,7 +328,11 @@ mod tests {
#[test] #[test]
fn render_bad_context() { fn render_bad_context() {
let (body, status) = render("node", &tera::Context::default(), None); let (body, status) =
match render("node", &tera::Context::default(), None) {
Ok(rendered) => panic!("Got Ok, expected Error"),
Err(error) => (error.template.html, error.template.code),
};
assert!(body.matches("Template render failed.").count() > 0); assert!(body.matches("Template render failed.").count() > 0);
assert_eq!(status, 500); assert_eq!(status, 500);
} }