Skip to content

Commit 473b922

Browse files
committed
Add axum example by translating the actix-web example
1 parent 411fa77 commit 473b922

File tree

15 files changed

+510
-3
lines changed

15 files changed

+510
-3
lines changed

.github/workflows/rust.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ jobs:
2626
- run: |
2727
set -eu
2828
for PKG in \
29-
examples/actix-web-app fuzzing rinja rinja_derive rinja_derive_standalone rinja_parser testing testing-alloc testing-no-std
29+
examples/actix-web-app examples/axum-app \
30+
fuzzing rinja rinja_derive rinja_derive_standalone rinja_parser \
31+
testing testing-alloc testing-no-std
3032
do
3133
cd "$PKG"
3234
echo "Testing: $PKG"
@@ -154,8 +156,9 @@ jobs:
154156
strategy:
155157
matrix:
156158
package: [
157-
examples/actix-web-app, fuzzing, rinja, rinja_derive, rinja_derive_standalone,
158-
rinja_parser, testing, testing-alloc, testing-no-std,
159+
examples/actix-web-app, examples/axum-app, fuzzing,
160+
rinja, rinja_derive, rinja_derive_standalone, rinja_parser,
161+
testing, testing-alloc, testing-no-std,
159162
]
160163
runs-on: ubuntu-latest
161164
steps:

examples/axum-app/.rustfmt.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../.rustfmt.toml

examples/axum-app/Cargo.toml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
2+
[package]
3+
name = "axum-app"
4+
version = "0.3.5"
5+
edition = "2021"
6+
license = "MIT OR Apache-2.0"
7+
publish = false
8+
9+
# This is an example application that uses both rinja as template engine,
10+
# and axum as your web-framework.
11+
[dependencies]
12+
axum = "0.8.1"
13+
rinja = { version = "0.3.5", path = "../../rinja" }
14+
tokio = { version = "1.43.0", features = ["macros", "rt-multi-thread"] }
15+
16+
# serde and strum are used to parse (deserialize) and generate (serialize) information
17+
# between web requests, e.g. to share the selected display language.
18+
serde = { version = "1.0.217", features = ["derive"] }
19+
strum = { version = "0.26.3", features = ["derive"] }
20+
21+
# These depenendies are simply used for a better user experience, having access logs in the
22+
# console, and error messages if anything goes wrong, e.g. if the port is already in use.
23+
displaydoc = "0.2.5"
24+
pretty-error-debug = "0.3.1"
25+
thiserror = "2.0.11"
26+
tower-http = { version = "0.6.2", features = ["trace"] }
27+
tracing = "0.1.41"
28+
tracing-subscriber = "0.3.19"
29+
30+
# In a real application you would not need this section. It is only used in here, so that this
31+
# example can have a more lenient MSRV (minimum supported rust version) than rinja as a whole.
32+
[workspace]
33+
members = ["."]

examples/axum-app/LICENSE-APACHE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../LICENSE-APACHE

examples/axum-app/LICENSE-MIT

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../LICENSE-MIT

examples/axum-app/README.md

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

examples/axum-app/_typos.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../_typos.toml

examples/axum-app/deny.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../deny.toml

examples/axum-app/src/main.rs

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
use axum::extract::{Path, Query};
2+
use axum::http::StatusCode;
3+
use axum::response::{Html, IntoResponse, Redirect, Response};
4+
use axum::routing::get;
5+
use axum::{Router, serve};
6+
use rinja::Template;
7+
use serde::Deserialize;
8+
use tower_http::trace::TraceLayer;
9+
use tracing::{Level, info};
10+
11+
#[tokio::main]
12+
async fn main() -> Result<(), Error> {
13+
tracing_subscriber::fmt()
14+
.with_max_level(Level::DEBUG)
15+
.init();
16+
17+
let app = Router::new()
18+
.route("/", get(start_handler))
19+
.route("/{lang}/index.html", get(index_handler))
20+
.route("/{lang}/greet-me.html", get(greeting_handler))
21+
.fallback(|| async { AppError::NotFound })
22+
.layer(TraceLayer::new_for_http());
23+
24+
// In a real application you would most likely read the configuration from a config file.
25+
let listener = tokio::net::TcpListener::bind("127.0.0.1:8000")
26+
.await
27+
.map_err(Error::Bind)?;
28+
29+
if let Ok(addr) = listener.local_addr() {
30+
info!("Listening on http://{addr}/");
31+
}
32+
serve(listener, app).await.map_err(Error::Run)
33+
}
34+
35+
#[derive(displaydoc::Display, pretty_error_debug::Debug, thiserror::Error)]
36+
enum Error {
37+
/// could not bind socket
38+
Bind(#[source] std::io::Error),
39+
/// could not run server
40+
Run(#[source] std::io::Error),
41+
}
42+
43+
/// Using this type your user can select the display language of your page.
44+
///
45+
/// The same type is used by axum as part of the URL, and in rinja to select what content to show,
46+
/// and also as an HTML attribute in `<html lang=`. To make it possible to use the same type for
47+
/// three different use cases, we use a few derive macros:
48+
///
49+
/// * `Default` to have a default/fallback language.
50+
/// * `Debug` is not strictly needed, but it might aid debugging.
51+
/// * `Clone` + `Copy` so that we can pass the language by value.
52+
/// * `PartialEq` so that we can use the type in comparisons with `==` or `!=`.
53+
/// * `serde::Deserialize` so that axum can parse the type in incoming URLs.
54+
/// * `strum::Display` so that rinja can write the value in templates.
55+
#[derive(Default, Debug, Clone, Copy, PartialEq, Deserialize, strum::Display)]
56+
#[allow(non_camel_case_types)]
57+
enum Lang {
58+
#[default]
59+
en,
60+
de,
61+
fr,
62+
}
63+
64+
/// This enum contains any error that could occur while handling an incoming request.
65+
///
66+
/// In a real application you would most likely have multiple error sources, e.g. database errors,
67+
#[derive(Debug, displaydoc::Display, thiserror::Error)]
68+
enum AppError {
69+
/// not found
70+
NotFound,
71+
/// could not render template
72+
Render(#[from] rinja::Error),
73+
}
74+
75+
/// This is your error handler
76+
impl IntoResponse for AppError {
77+
fn into_response(self) -> Response {
78+
// It uses a rinja template to display its content.
79+
// The member `lang` is used by "_layout.html" which "error.html" extends. Even though it
80+
// is always the fallback language English in here, "_layout.html" expects to be able to
81+
// access this field, so you have to provide it.
82+
#[derive(Debug, Template)]
83+
#[template(path = "error.html")]
84+
struct Tmpl {
85+
lang: Lang,
86+
err: AppError,
87+
}
88+
89+
let status = match &self {
90+
AppError::NotFound => StatusCode::NOT_FOUND,
91+
AppError::Render(_) => StatusCode::INTERNAL_SERVER_ERROR,
92+
};
93+
let tmpl = Tmpl {
94+
lang: Lang::default(),
95+
err: self,
96+
};
97+
if let Ok(body) = tmpl.render() {
98+
(status, Html(body)).into_response()
99+
} else {
100+
(status, "Something went wrong").into_response()
101+
}
102+
}
103+
}
104+
105+
/// The is first page your user hits does not contain language information, so we redirect them
106+
/// to a URL that does contain the default language.
107+
async fn start_handler() -> Redirect {
108+
Redirect::temporary("/en/index.html")
109+
}
110+
111+
/// This type collects the query parameter `?name=` (if present)
112+
#[derive(Debug, Deserialize)]
113+
struct IndexHandlerQuery {
114+
#[serde(default)]
115+
name: String,
116+
}
117+
118+
/// This is the first localized page your user sees.
119+
///
120+
/// It has arguments in the path that need to be parsable using `serde::Deserialize`; see `Lang`
121+
/// for an explanation. And also query parameters (anything after `?` in the incoming URL).
122+
async fn index_handler(
123+
Path((lang,)): Path<(Lang,)>,
124+
Query(query): Query<IndexHandlerQuery>,
125+
) -> Result<Response, AppError> {
126+
// In the template we both use `{% match lang %}` and `{% if lang !=`, the former to select the
127+
// text of a specific language, e.g. in the `<title>`; and the latter to display references to
128+
// all other available languages except the currently selected one.
129+
// The field `name` will contain the value of the query parameter of the same name.
130+
// In `IndexHandlerQuery` we annotated the field with `#[serde(default)]`, so if the value is
131+
// absent, an empty string is selected by default, which is visible to the user an empty
132+
// `<input type="text" />` element.
133+
#[derive(Debug, Template)]
134+
#[template(path = "index.html")]
135+
struct Tmpl {
136+
lang: Lang,
137+
name: String,
138+
}
139+
140+
let template = Tmpl {
141+
lang,
142+
name: query.name,
143+
};
144+
Ok(Html(template.render()?).into_response())
145+
}
146+
147+
#[derive(Debug, Deserialize)]
148+
struct GreetingHandlerQuery {
149+
name: String,
150+
}
151+
152+
/// This is the final page of this example application.
153+
///
154+
/// Like `index_handler` it contains a language in the URL, and a query parameter to read the user's
155+
/// provided name. In here, the query argument `name` has no default value, so axum will show
156+
/// an error message if absent.
157+
async fn greeting_handler(
158+
Path((lang,)): Path<(Lang,)>,
159+
Query(query): Query<GreetingHandlerQuery>,
160+
) -> Result<Response, AppError> {
161+
#[derive(Debug, Template)]
162+
#[template(path = "greet.html")]
163+
struct Tmpl {
164+
lang: Lang,
165+
name: String,
166+
}
167+
168+
let template = Tmpl {
169+
lang,
170+
name: query.name,
171+
};
172+
Ok(Html(template.render()?).into_response())
173+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{#-
2+
This file is included by "_layout.html".
3+
You can still use template syntax (such as this comment) in here.
4+
-#}
5+
6+
html {
7+
background-color: #eee;
8+
color: #111;
9+
font-size: 62.5%;
10+
min-height: 100vh;
11+
color-scheme: light;
12+
}
13+
* {
14+
line-height: 1.2em;
15+
}
16+
body {
17+
background-color: #fff;
18+
font-size: 1.8rem;
19+
max-width: 40em;
20+
margin: 1em auto;
21+
padding: 2em;
22+
}
23+
h1 { font-size: 2.4rem; }
24+
h2 { font-size: 2.2rem; }
25+
h3 { font-size: 2.0rem; }
26+
a:link, a:visited {
27+
color: #36c;
28+
text-decoration: none;
29+
}
30+
a:active, a:hover, a:focus {
31+
text-decoration: underline;
32+
text-underline-offset: 0.3em;
33+
}
34+
#lang-select {
35+
font-size: 80%;
36+
width: max-content;
37+
margin: 2em 0 0 auto;
38+
display: flex;
39+
flex-direction: row;
40+
flex-wrap: wrap;
41+
}
42+
#lang-select li {
43+
flex-grow: 1;
44+
flex-basis: auto;
45+
margin: .25em 0 0 0;
46+
padding: 0 1em;
47+
text-align: center;
48+
list-style-type: none;
49+
border-left: 0.1rem solid currentColor;
50+
}
51+
#lang-select li:first-of-type {
52+
border-left: 0 none transparent;
53+
}
54+
#lang-select li:last-of-type {
55+
padding-right: 0;
56+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
{#-
2+
This is the basic layout of our example application.
3+
It is the core skeleton shared between all pages.
4+
It expects the struct of any template that `{% extends %}` this layout to contain
5+
(at least) a field `lang: Lang`, so it can be used in the `<html lang=` attribute.
6+
-#}
7+
8+
<!DOCTYPE html>
9+
<html lang="{{lang}}">
10+
<head>
11+
<meta charset="UTF-8" />
12+
{#-
13+
A base template can contain `blocks`, which my be overridden templates that use
14+
this base template. A block may contain a default content, if the extending
15+
template does want to / need to override the content of a block.
16+
17+
E.g. maybe you would like to have "Rinja example application" as default title for
18+
your pages, then simply add this text (without quotation marks) in the block!
19+
20+
The default content can be as complex as you need it to be.
21+
E.g. it may contain any nodes like `{% if … %}`, and even other blocks.
22+
~#}
23+
<title>{% block title %}{% endblock %}</title>
24+
25+
<meta http-equiv="expires" content="Sat, 01 Dec 2001 00:00:00 GMT" />
26+
<meta http-equiv="cache-control" content="no-cache, no-store, must-revalidate" />
27+
<meta http-equiv="pragma" content="no-cache" />
28+
<meta name="viewport" content="width=device-width, initial-scale=1" />
29+
<meta name="robots" content="noindex, nofollow" />
30+
31+
{#-
32+
In a real application you most likely would want to link style sheets,
33+
any JavaScripts etc. using e.g. `actix-files`, instead of embedding the content
34+
in your generated HTML.
35+
36+
As you can see, this comment starts with `-`, which will tells the comment
37+
to strip all white spaces before it, until it finds the first non-white space
38+
character, a `>`.
39+
40+
The comment is also terminated with `~`. This also strips white spaces, but
41+
will leave one space, or a newline character, if the stripped content contained
42+
a newline.
43+
~#}
44+
<style>
45+
/*<![CDATA[*/
46+
{%~ include "_layout.css" ~%}
47+
/*]]>*/
48+
</style>
49+
</head>
50+
<body>
51+
{%~ block content %}{% endblock ~%}
52+
</body>
53+
</html>
54+
55+
{%- macro lang_select(page, query="") -%}
56+
<ul id="lang-select">
57+
{%- if lang != Lang::en -%}
58+
<li><a href="/en/{{ page }}.html{{ query }}">This page in English</a></li>
59+
{%- endif -%}
60+
{%- if lang != Lang::de -%}
61+
<li><a href="/de/{{ page }}.html{{ query }}">Diese Seite auf deutsch.</a></li>
62+
{%- endif -%}
63+
{%- if lang != Lang::fr -%}
64+
<li><a href="/fr/{{ page }}.html{{ query }}">Cette page est en français.</a></li>
65+
{%- endif -%}
66+
</ul>
67+
{%- endmacro lang_select -%}

0 commit comments

Comments
 (0)