Add comments

This commit is contained in:
René Kijewski 2024-07-07 20:47:02 +02:00
parent cf94c09e32
commit fafd44e4fe
6 changed files with 200 additions and 61 deletions

View File

@ -2,19 +2,31 @@
name = "actix-web-app"
version = "0.1.0"
edition = "2021"
license = "MIT OR Apache-2.0"
publish = false
[dependencies]
# This is an example application that uses both rinja as template engine,
# and actix-web as your web-framework.
# rinja_actix makes it easy to use rinja templates as `Responder` of an actix-web request.
# The rendered template is simply the response of your handler!
rinja_actix = { version = "0.15.0", path = "../../rinja_actix" }
actix-web = { version = "4.8.0", default-features = false, features = ["macros"] }
tokio = { version = "1.38.0", features = ["sync", "rt-multi-thread"] }
# serde and strum are used to parse (deserialize) and generate (serialize) information
# between web requests, e.g. to share the selected display language.
serde = { version = "1.0.203", features = ["derive"] }
strum = { version = "0.26.3", features = ["derive"] }
# These depenendies are simply used for a better user experience, having access logs in the
# console, and error messages if anything goes wrong, e.g. if the port is already in use.
env_logger = "0.11.3"
log = "0.4.22"
pretty-error-debug = "0.3.0"
serde = { version = "1.0.203", features = ["derive"] }
strum = { version = "0.26.3", features = ["derive"] }
thiserror = "1.0.61"
tokio = { version = "1.38.0", features = ["sync", "rt-multi-thread"] }
# In a real application you would not need this section. It is only used in here, so that this
# example can have a more lenient MSRV (minimum supported rust version) than rinja as a whole.
[workspace]
members = ["."]

View File

@ -0,0 +1,17 @@
# rinja + actix-web example web app
This is a simple web application that uses rinja as template engine, and
[actix-web](https://crates.io/crates/actix-web) as web framework.
It lets the user of the web page select a display language, and asks for their name.
The example shows the interaction between both projects, and serves as an example to use
basic rinja features such as base templates to a unified layout skeletton for your page,
and less boilerplate in your template code.
To run the example execute `cargo run` in this folder.
Once the project is running, open <http://127.0.0.1:8080/> in your browser.
To gracefully shut does the server, type ctrl+C in your terminal.
The files of the project contain comments for you to read.
The recommended reading order is "templates/_layout.html", "templates/index.html",
"Cargo.toml", "src/main.rs". Also please have a look at our [book](https://rinja.readthedocs.io/),
which explains rinja's features in greater detail.

View File

@ -1,11 +1,12 @@
use actix_web::http::{header, Method};
use actix_web::{
get, middleware, web, App, Either, HttpRequest, HttpResponse, HttpServer, Responder, Result,
get, middleware, web, App, HttpRequest, HttpResponse, HttpServer, Responder, Result,
};
use rinja_actix::Template;
use serde::Deserialize;
use tokio::runtime;
// This function and the next mostly contains boiler plate to get an actix-web application running.
fn main() -> Result<(), Error> {
let env = env_logger::Env::new().default_filter_or("info");
env_logger::try_init_from_env(env).map_err(Error::Log)?;
@ -19,6 +20,7 @@ fn main() -> Result<(), Error> {
async fn amain() -> Result<(), Error> {
let server = HttpServer::new(|| {
// This closure contains the setup of the routing rules of your app.
App::new()
.wrap(middleware::Logger::default())
.wrap(middleware::NormalizePath::new(
@ -29,6 +31,7 @@ async fn amain() -> Result<(), Error> {
.service(greeting_handler)
.default_service(web::to(not_found_handler))
});
// In a real application you would most likely read the configuration from a config file.
let server = server.bind(("127.0.0.1", 8080)).map_err(Error::Bind)?;
for addr in server.addrs() {
println!("Listening on: http://{addr}/");
@ -48,7 +51,20 @@ enum Error {
Run(#[source] std::io::Error),
}
#[derive(Default, Debug, Clone, Copy, PartialEq, Deserialize, strum::Display, strum::AsRefStr)]
/// Using this type your user can select the display language of your page.
///
/// The same type is used by actix-web as part of the URL, and in rinja 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 diffent 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 actix-web can parse the type in incoming URLs.
/// * `strum::AsRefStr` so that actix-web the use the type to construct URL for printing.
/// * `strum::Display` so that rinja can write the value in templates.
#[derive(Default, Debug, Clone, Copy, PartialEq, Deserialize, strum::AsRefStr, strum::Display)]
#[allow(non_camel_case_types)]
enum Lang {
#[default]
@ -57,7 +73,13 @@ enum Lang {
fr,
}
/// This is your "Error: 404 - not found" handler
async fn not_found_handler(req: HttpRequest) -> Result<impl Responder> {
// It uses a rinja template to display its content.
// The member `req` contains the request, and is used e.g. to generate URLs in our template.
// The member `lang` is used by "_layout.html" which "404.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 = "404.html")]
struct Tmpl {
@ -65,35 +87,62 @@ async fn not_found_handler(req: HttpRequest) -> Result<impl Responder> {
lang: Lang,
}
match req.method() {
&Method::GET => Ok(Either::Left(Tmpl {
if req.method() == Method::GET {
// In here we have to render the result to a string manually, because we don't want to
// generate a "status 200" result, but "status 404". In other cases you can simply return
// the template, wrapped in `Ok()`, and the request gets generated with "status 200",
// and the right MIME type.
let tmpl = Tmpl {
req,
lang: Lang::default(),
})),
_ => Ok(Either::Right(HttpResponse::MethodNotAllowed().finish())),
};
// The MIME type was derived by rinja by the extension of the template file.
Ok(HttpResponse::NotFound()
.append_header((header::CONTENT_TYPE, Tmpl::MIME_TYPE))
.body(tmpl.to_string()))
} else {
Ok(HttpResponse::MethodNotAllowed().finish())
}
}
/// The is first page your user hits does not contain language infomation, so we redirect them
/// to a URL that does contain the default language.
#[get("/")]
async fn start_handler(req: HttpRequest) -> Result<impl Responder> {
// This example show how the type `Lang` can be used to construct a URL in actix-web.
let url = req.url_for("index_handler", [Lang::default()])?;
Ok(HttpResponse::Found()
.insert_header((header::LOCATION, url.as_str()))
.finish())
}
/// This type collects the query parameter `?name=` (if present)
#[derive(Debug, Deserialize)]
struct IndexHandlerQuery {
#[serde(default)]
name: String,
}
/// 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).
#[get("/{lang}/index.html")]
async fn index_handler(
req: HttpRequest,
path: web::Path<(Lang,)>,
web::Query(query): web::Query<IndexHandlerQuery>,
) -> Result<impl Responder> {
// Same as in `not_found_handler`, we have `req` to build URLs in the template, and
// `lang` to select the display language. 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 paramater 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 {
@ -115,6 +164,11 @@ struct GreetingHandlerQuery {
name: String,
}
/// 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 actix-web will show
/// an error message if absent.
#[get("/{lang}/greet-me.html")]
async fn greeting_handler(
req: HttpRequest,

View File

@ -0,0 +1,56 @@
{#-
This file is included by "_layout.html".
You can still use template syntax (such as this comment) in here.
-#}
html {
background-color: #eee;
color: #111;
font-size: 62.5%;
min-height: 100vh;
color-scheme: light;
}
* {
line-height: 1.2em;
}
body {
background-color: #fff;
font-size: 1.8rem;
max-width: 40em;
margin: 1em auto;
padding: 2em;
}
h1 { font-size: 2.4rem; }
h2 { font-size: 2.2rem; }
h3 { font-size: 2.0rem; }
a:link, a:visited {
color: #36c;
text-decoration: none;
}
a:active, a:hover, a:focus {
text-decoration: underline;
text-underline-offset: 0.3em;
}
#lang-select {
font-size: 80%;
width: max-content;
margin: 2em 0 0 auto;
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
#lang-select li {
flex-grow: 1;
flex-basis: auto;
margin: .25em 0 0 0;
padding: 0 1em;
text-align: center;
list-style-type: none;
border-left: 0.1rem solid currentColor;
}
#lang-select li:first-of-type {
border-left: 0 none transparent;
}
#lang-select li:last-of-type {
padding-right: 0;
}

View File

@ -1,67 +1,49 @@
{#-
This is the basic layout of our example application.
It is the core skeletton shared between all pages.
It expects the struct of any template that `{% extends %}` this layout to contain
(at least) a field `lang: Lang`, so it can be used in the `<html lang=` attribute.
-#}
<!DOCTYPE html>
<html lang="{{lang}}">
<head>
<meta charset="UTF-8" />
{#-
A base template can contain `blocks`, which my be overridden templates that use
this base template. A block may contain a default content, if the extending
template does want to / need to override the content of a block.
E.g. maybe you would like to have "Rinja example application" as default title for
your pages, then simply add this text (without quotation marks) in the block!
The default content can be as complex as you need to to be.
E.g. it may contain any nodes like `{% if … %}`, and even other blocks.
~#}
<title>{% block title %}{% endblock %}</title>
<meta http-equiv="expires" content="Sat, 01 Dec 2001 00:00:00 GMT" />
<meta http-equiv="cache-control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="pragma" content="no-cache" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="robots" content="noindex, nofollow" />
{#-
In a real application you most likely would want to link style sheets,
any JavaScripts etc. using e.g. `actix-files`, instead of embedding the content
in your generated HTML.
As you can see, this comment starts with `-`, which will tells the comment
to strip all white spaces before it, until it finds the first non-white space
character, a `>`.
The comment is also terminated with `~`. This also strips white spaces, but
will leave one space, or a newline character, if the stripped content contained
a newline.
~#}
<style>
/*<![CDATA[*/
html {
background-color: #eee;
color: #111;
font-size: 62.5%;
min-height: 100vh;
color-scheme: light;
}
* {
line-height: 1.2em;
}
body {
background-color: #fff;
font-size: 1.8rem;
max-width: 40em;
margin: 1em auto;
padding: 2em;
}
h1 { font-size: 2.4rem; }
h2 { font-size: 2.2rem; }
h3 { font-size: 2.0rem; }
a:link, a:visited {
color: #36c;
text-decoration: none;
}
a:active, a:hover, a:focus {
text-decoration: underline;
text-underline-offset: 0.3em;
}
#lang-select {
font-size: 80%;
width: max-content;
margin: 2em 0 0 auto;
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
#lang-select li {
flex-grow: 1;
flex-basis: auto;
margin: .25em 0 0 0;
padding: 0 1em;
text-align: center;
list-style-type: none;
border-left: 0.1rem solid currentColor;
}
#lang-select li:first-of-type {
border-left: 0 none transparent;
}
#lang-select li:last-of-type {
padding-right: 0;
}
{%~ include "_layout.css" ~%}
/*]]>*/
</style>
</head>

View File

@ -1,6 +1,9 @@
{% extends "_layout.html" %}
{%- block title -%}
{#-
In here you can see how to use the language URL compment to select the text to display.
-#}
{%- match lang -%}
{%- when Lang::en -%} Hello!
{%- when Lang::de -%} Hallo!
@ -16,12 +19,27 @@
{%- when Lang::fr -%} Bonjour!
{%- endmatch -%}
</h1>
{#-
The `action` URL is built by actix-web, by using the user request `req` and
the language component of the URL. Both are fields in the struct that uses
this template file.
~#}
<form
method="GET"
action="{{ req.url_for("greeting_handler", [lang])? }}"
autocomplete="off"
>
<p>
{#-
If your text contains long lines, you may want to split them,
so you as a developer have an easier time reading them.
If you don't want to end up with a multitude of spaces in the
generated content, you can use empty comments as seen below: `#-~#`.
This would strip the space before the comment, and leave a newline
character after the comment. Another option would be `#~-#`,
so that single space remains.
-#}
{%- match lang -%}
{%- when Lang::en -%}
I would like to say <em>hello</em>. {#-~#}