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 + // `<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, 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 `<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 %} + + {#- + 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. + ~#}

+ {#- + 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 hello. {#-~#}