repos
/ finance-rust master

finance-rust

mirror archived upstream

Single-binary self-hosted market watcher for stocks, ETFs, indexes, and futures: live charts, key stats, fundamentals, SEC filings, and SSE streaming.

axumdockerfinancerustself-hostedsqlitestocksvite

3.5 KB · 98 lines · Rust Raw History
 1use axum::{
 2    http::StatusCode,
 3    response::{Html, IntoResponse, Response},
 4};
 5use chrono::Datelike;
 6
 7use crate::templates::RequestCtx;
 8use crate::AppState;
 9
10/// Render a template to a String with the standard page context injected.
11///
12/// Every template can rely on `request`, `now`, `site`, and `base_url` being
13/// present; callers pass page-specific fields through `extra`.
14pub fn render_to_string(
15    state: &AppState,
16    template: &str,
17    path: &str,
18    extra: minijinja::Value,
19) -> Result<String, Response> {
20    let tmpl = state.env.get_template(template).map_err(|e| {
21        tracing::error!("template '{}': {}", template, e);
22        server_error(state, path, &format!("template '{template}': {e}"))
23    })?;
24    tmpl.render(minijinja::context! {
25        request => RequestCtx { path: path.to_string() },
26        now => minijinja::context! { year => chrono::Local::now().year() },
27        site => minijinja::context! {
28            title => &state.config.site_title,
29            base_url => &state.config.base_url,
30        },
31        base_url => &state.config.base_url,
32        ..extra
33    })
34    .map_err(|e| {
35        tracing::error!("render '{}': {}", template, e);
36        // minijinja's `Display` carries the bare error; the source chain carries
37        // the location and line span, which is what the operator needs to look at.
38        let mut detail = format!("render '{template}': {e}");
39        let mut source = std::error::Error::source(&e);
40        while let Some(s) = source {
41            detail.push_str(&format!("\n  caused by: {s}"));
42            source = s.source();
43        }
44        server_error(state, path, &detail)
45    })
46}
47
48/// Convenience wrapper around `render_to_string` for HTML responses.
49pub fn render(state: &AppState, template: &str, path: &str, extra: minijinja::Value) -> Response {
50    match render_to_string(state, template, path, extra) {
51        Ok(body) => Html(body).into_response(),
52        Err(resp) => resp,
53    }
54}
55
56/// The themed 404 page with a 404 status. Used by the router fallback and by
57/// routes that look up a missing resource (e.g. an unknown ticker).
58pub fn not_found(state: &AppState) -> Response {
59    let body = render(
60        state,
61        "pages/not_found.html",
62        "/404",
63        minijinja::context! { title => "Not found" },
64    );
65    (StatusCode::NOT_FOUND, body).into_response()
66}
67
68/// The themed 500 page with the underlying error detail. Single-operator app
69/// (no public sign-up), so leaking the message back is fine.
70/// It is the whole point of the page. Falls back to plain text if the error
71/// page itself fails to render, so we never recurse.
72fn server_error(state: &AppState, path: &str, detail: &str) -> Response {
73    let ctx = minijinja::context! {
74        title => "Page failed to render",
75        path => path,
76        detail => detail,
77    };
78    let body = state
79        .env
80        .get_template("pages/error.html")
81        .and_then(|t| {
82            t.render(minijinja::context! {
83                request => RequestCtx { path: path.to_string() },
84                now => minijinja::context! { year => chrono::Local::now().year() },
85                site => minijinja::context! {
86                    title => &state.config.site_title,
87                    base_url => &state.config.base_url,
88                },
89                base_url => &state.config.base_url,
90                ..ctx
91            })
92        });
93    match body {
94        Ok(html) => (StatusCode::INTERNAL_SERVER_ERROR, Html(html)).into_response(),
95        Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, detail.to_string()).into_response(),
96    }
97}