mirror of
https://github.com/askama-rs/askama.git
synced 2025-10-01 06:51:15 +00:00
200 lines
6.8 KiB
Rust
200 lines
6.8 KiB
Rust
use std::net::{IpAddr, Ipv4Addr};
|
|
|
|
use askama::Template;
|
|
use salvo::catcher::Catcher;
|
|
use salvo::conn::TcpListener;
|
|
use salvo::http::StatusCode;
|
|
use salvo::logging::Logger;
|
|
use salvo::macros::Extractible;
|
|
use salvo::writing::{Redirect, Text};
|
|
use salvo::{Listener, Request, Response, Router, Scribe, Server, Service, handler};
|
|
use serde::Deserialize;
|
|
use tracing::Level;
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<(), Error> {
|
|
tracing_subscriber::fmt()
|
|
.with_max_level(Level::DEBUG)
|
|
.init();
|
|
|
|
let router = Router::new()
|
|
.push(Router::new().get(start_handler))
|
|
.push(Router::with_path("{lang}/index.html").get(index_handler))
|
|
.push(Router::with_path("{lang}/greet-me.html").get(greeting_handler));
|
|
let server = Service::new(router)
|
|
.catcher(Catcher::default().hoop(not_found_handler))
|
|
.hoop(Logger::new());
|
|
|
|
// In a real application you would most likely read the configuration from a config file.
|
|
let acceptor = TcpListener::new((IpAddr::V4(Ipv4Addr::LOCALHOST), 8080))
|
|
.try_bind()
|
|
.await
|
|
.map_err(Error::Bind)?;
|
|
|
|
Server::new(acceptor)
|
|
.try_serve(server)
|
|
.await
|
|
.map_err(Error::Run)
|
|
}
|
|
|
|
#[derive(displaydoc::Display, thiserror::Error, pretty_error_debug::Debug)]
|
|
enum Error {
|
|
/// could not bind socket
|
|
Bind(#[source] salvo::Error),
|
|
/// could not run server
|
|
Run(#[source] std::io::Error),
|
|
}
|
|
|
|
/// Thanks to this type, your user can select the display language of your page.
|
|
///
|
|
/// The same type is used by salvo as part of the URL, and in askama to select what content to show,
|
|
/// and also as an HTML attribute in `<html lang=`. To make it possible to use the same type for
|
|
/// three different use cases, we use a few derive macros:
|
|
///
|
|
/// * `Default` to have a default/fallback language.
|
|
/// * `Debug` is not strictly needed, but it might aid debugging.
|
|
/// * `Clone` + `Copy` so that we can pass the language by value.
|
|
/// * `PartialEq` so that we can use the type in comparisons with `==` or `!=`.
|
|
/// * `serde::Deserialize` so that salvo can parse the type in incoming URLs.
|
|
/// * `strum::Display` so that askama can write the value in templates.
|
|
#[derive(Default, Debug, Clone, Copy, PartialEq, Deserialize, strum::Display)]
|
|
#[allow(non_camel_case_types)]
|
|
enum Lang {
|
|
#[default]
|
|
en,
|
|
de,
|
|
fr,
|
|
}
|
|
|
|
/// This enum contains any error that could occur while handling an incoming request.
|
|
///
|
|
/// In a real application you would most likely have multiple error sources, e.g. database errors,
|
|
#[derive(Debug, displaydoc::Display, thiserror::Error)]
|
|
enum AppError {
|
|
/// not found
|
|
NotFound,
|
|
/// could not extract information from request
|
|
Extract(#[from] salvo::http::ParseError),
|
|
/// could not render template
|
|
Render(#[from] askama::Error),
|
|
}
|
|
|
|
/// This is your error handler
|
|
impl Scribe for AppError {
|
|
fn render(self, res: &mut Response) {
|
|
// It uses an askama template to display its content.
|
|
// The member `lang` is used by "_layout.html" which "error.html" extends. Even though it
|
|
// is always the fallback language English in here, "_layout.html" expects to be able to
|
|
// access this field, so you have to provide it.
|
|
#[derive(Debug, Template)]
|
|
#[template(path = "error.html")]
|
|
struct Tmpl {
|
|
lang: Lang,
|
|
err: AppError,
|
|
}
|
|
|
|
res.status_code(match &self {
|
|
AppError::NotFound => StatusCode::NOT_FOUND,
|
|
AppError::Extract(_) => StatusCode::UNPROCESSABLE_ENTITY,
|
|
AppError::Render(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
|
});
|
|
let tmpl = Tmpl {
|
|
lang: Lang::default(),
|
|
err: self,
|
|
};
|
|
if let Ok(body) = tmpl.render() {
|
|
Text::Html(body).render(res);
|
|
} else {
|
|
Text::Plain("Something went wrong").render(res);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// This is your "Error: 404 - not found" handler
|
|
#[handler]
|
|
async fn not_found_handler() -> impl Scribe {
|
|
AppError::NotFound
|
|
}
|
|
|
|
/// This is the first page your user hits, meaning it does not contain language information,
|
|
/// so we redirect them.
|
|
#[handler]
|
|
async fn start_handler() -> impl Scribe {
|
|
Redirect::found("/en/index.html")
|
|
}
|
|
|
|
/// This is the first localized page your user sees.
|
|
///
|
|
/// It has arguments in the path that need to be parsable using `serde::Deserialize`; see `Lang`
|
|
/// for an explanation. And also query parameters (anything after `?` in the incoming URL).
|
|
#[handler]
|
|
async fn index_handler(req: &mut Request) -> Result<impl Scribe, AppError> {
|
|
/// This type collects the URL params, i.e. the `"/{lang}/"` part
|
|
#[derive(Debug, Deserialize, Extractible)]
|
|
#[salvo(extract(default_source(from = "param")))]
|
|
struct Params {
|
|
lang: Lang,
|
|
}
|
|
|
|
/// This type collects the query parameter `?name=` (if present)
|
|
#[derive(Debug, Deserialize, Extractible)]
|
|
#[salvo(extract(default_source(from = "query")))]
|
|
struct Query {
|
|
#[serde(default)]
|
|
name: String,
|
|
}
|
|
|
|
let Params { lang } = req.extract().await?;
|
|
let Query { name } = req.extract().await?;
|
|
|
|
// In the template we both use `{% match lang %}` and `{% if lang !=`, the former to select the
|
|
// text of a specific language, e.g. in the `<title>`; and the latter to display references to
|
|
// all other available languages except the currently selected one.
|
|
// The field `name` will contain the value of the query parameter of the same name.
|
|
// In `IndexHandlerQuery` we annotated the field with `#[serde(default)]`, so if the value is
|
|
// absent, an empty string is selected by default, which is visible to the user an empty
|
|
// `<input type="text" />` element.
|
|
#[derive(Debug, Template)]
|
|
#[template(path = "index.html")]
|
|
struct Tmpl {
|
|
lang: Lang,
|
|
name: String,
|
|
}
|
|
|
|
let template = Tmpl { lang, name };
|
|
Ok(Text::Html(template.render()?))
|
|
}
|
|
|
|
/// This is the final page of this example application.
|
|
///
|
|
/// Like `index_handler` it contains a language in the URL, and a query parameter to read the user's
|
|
/// provided name. In here, the query argument `name` has no default value, so salvo will show
|
|
/// an error message if absent.
|
|
#[handler]
|
|
async fn greeting_handler(req: &mut Request) -> Result<impl Scribe, AppError> {
|
|
#[derive(Debug, Deserialize, Extractible)]
|
|
#[salvo(extract(default_source(from = "param")))]
|
|
struct Params {
|
|
lang: Lang,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Extractible)]
|
|
#[salvo(extract(default_source(from = "query")))]
|
|
struct Query {
|
|
name: String,
|
|
}
|
|
|
|
let Params { lang } = req.extract().await?;
|
|
let Query { name } = req.extract().await?;
|
|
|
|
#[derive(Debug, Template)]
|
|
#[template(path = "greet.html")]
|
|
struct Tmpl {
|
|
lang: Lang,
|
|
name: String,
|
|
}
|
|
|
|
let template = Tmpl { lang, name };
|
|
Ok(Text::Html(template.render()?))
|
|
}
|