diff --git a/examples/actix-web-app/Cargo.toml b/examples/actix-web-app/Cargo.toml
index cfdf5081..87509d4f 100644
--- a/examples/actix-web-app/Cargo.toml
+++ b/examples/actix-web-app/Cargo.toml
@@ -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 = ["."]
diff --git a/examples/actix-web-app/README.md b/examples/actix-web-app/README.md
new file mode 100644
index 00000000..ce89481e
--- /dev/null
+++ b/examples/actix-web-app/README.md
@@ -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 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.
diff --git a/examples/actix-web-app/src/main.rs b/examples/actix-web-app/src/main.rs
index 3598708f..e232abf6 100644
--- a/examples/actix-web-app/src/main.rs
+++ b/examples/actix-web-app/src/main.rs
@@ -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 ` Result {
+ // 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 {
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 {
+ // 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,
) -> Result {
+ // 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 ``;
+ // 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
+ // `` 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,
diff --git a/examples/actix-web-app/templates/_layout.css b/examples/actix-web-app/templates/_layout.css
new file mode 100644
index 00000000..3b40a475
--- /dev/null
+++ b/examples/actix-web-app/templates/_layout.css
@@ -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;
+}
diff --git a/examples/actix-web-app/templates/_layout.html b/examples/actix-web-app/templates/_layout.html
index 4f8360c8..c7df08e4 100644
--- a/examples/actix-web-app/templates/_layout.html
+++ b/examples/actix-web-app/templates/_layout.html
@@ -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 `
+ {#-
+ 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.
+ ~#}
{% block title %}{% endblock %}
+
+ {#-
+ 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.
+ ~#}
diff --git a/examples/actix-web-app/templates/index.html b/examples/actix-web-app/templates/index.html
index 3d59830e..8cc9fba6 100644
--- a/examples/actix-web-app/templates/index.html
+++ b/examples/actix-web-app/templates/index.html
@@ -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 -%}
+ {#-
+ 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.
+ ~#}