diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index fbb4ad48..08d92d7e 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -26,8 +26,8 @@ jobs: - run: | set -eu for PKG in \ - examples/actix-web-app examples/axum-app \ - fuzzing rinja rinja_derive rinja_derive_standalone rinja_parser \ + examples/actix-web-app examples/axum-app examples/warp-app fuzzing \ + rinja rinja_derive rinja_derive_standalone rinja_parser \ testing testing-alloc testing-no-std do cd "$PKG" @@ -115,7 +115,9 @@ jobs: - run: | set -eu for PKG in \ - examples/actix-web-app fuzzing rinja rinja_derive rinja_derive_standalone rinja_parser testing testing-alloc testing-no-std + examples/actix-web-app examples/axum-app examples/warp-app fuzzing \ + rinja rinja_derive rinja_derive_standalone rinja_parser \ + testing testing-alloc testing-no-std do cd "$PKG" cargo sort --check --check-format --grouped @@ -156,7 +158,7 @@ jobs: strategy: matrix: package: [ - examples/actix-web-app, examples/axum-app, fuzzing, + examples/actix-web-app, examples/axum-app, examples/warp-app, fuzzing, rinja, rinja_derive, rinja_derive_standalone, rinja_parser, testing, testing-alloc, testing-no-std, ] diff --git a/examples/warp-app/.rustfmt.toml b/examples/warp-app/.rustfmt.toml new file mode 120000 index 00000000..fed79016 --- /dev/null +++ b/examples/warp-app/.rustfmt.toml @@ -0,0 +1 @@ +../../.rustfmt.toml \ No newline at end of file diff --git a/examples/warp-app/Cargo.toml b/examples/warp-app/Cargo.toml new file mode 100644 index 00000000..6f997b98 --- /dev/null +++ b/examples/warp-app/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "actix-web-app" +version = "0.3.5" +edition = "2021" +license = "MIT OR Apache-2.0" +publish = false + +# This is an example application that uses both rinja as template engine, +# and actix-web as your web-framework. +[dependencies] +http = "0.2.12" +rinja = { version = "0.3.5", path = "../../rinja" } +tokio = { version = "1.43.0", features = ["macros", "rt-multi-thread"] } +warp = "0.3.7" + +# 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.217", 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. +displaydoc = "0.2.5" +env_logger = "0.11.6" +pretty-error-debug = "0.3.1" +thiserror = "2.0.11" + +# 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/warp-app/LICENSE-APACHE b/examples/warp-app/LICENSE-APACHE new file mode 120000 index 00000000..1cd601d0 --- /dev/null +++ b/examples/warp-app/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/examples/warp-app/LICENSE-MIT b/examples/warp-app/LICENSE-MIT new file mode 120000 index 00000000..b2cfbdc7 --- /dev/null +++ b/examples/warp-app/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/examples/warp-app/README.md b/examples/warp-app/README.md new file mode 100644 index 00000000..63fbe469 --- /dev/null +++ b/examples/warp-app/README.md @@ -0,0 +1,17 @@ +# rinja + warp example web app + +This is a simple web application that uses rinja as template engine, and +[warp](https://crates.io/crates/warp) 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 skeleton 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/warp-app/_typos.toml b/examples/warp-app/_typos.toml new file mode 120000 index 00000000..15e4306a --- /dev/null +++ b/examples/warp-app/_typos.toml @@ -0,0 +1 @@ +../../_typos.toml \ No newline at end of file diff --git a/examples/warp-app/deny.toml b/examples/warp-app/deny.toml new file mode 120000 index 00000000..ae01e357 --- /dev/null +++ b/examples/warp-app/deny.toml @@ -0,0 +1 @@ +../../deny.toml \ No newline at end of file diff --git a/examples/warp-app/src/main.rs b/examples/warp-app/src/main.rs new file mode 100644 index 00000000..92813f01 --- /dev/null +++ b/examples/warp-app/src/main.rs @@ -0,0 +1,151 @@ +use std::net::Ipv4Addr; + +use http::{StatusCode, Uri}; +use rinja::Template; +use serde::Deserialize; +use warp::filters::query::query; +use warp::reply::{Reply, Response, html, with_status}; +use warp::{Filter, any, get, path, redirect, serve}; + +#[tokio::main] +async fn main() { + env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); + + let routes = path!() + .map(start_handler) + .or(path!(Lang / "index.html").and(query()).map(index_handler)) + .or(path!(Lang / "greet-me.html") + .and(query()) + .map(greeting_handler)) + .or(any().map(|| AppError::NotFound)); + let routes = get().and(routes).with(warp::log("warp-app")); + + // In a real application you would most likely read the configuration from a config file. + serve(routes).run((Ipv4Addr::LOCALHOST, 8080)).await; +} + +/// Thanks to this type, your user can select the display language of your page. +/// +/// The same type is used by warp as part of the URL, and in rinja to select what content to show, +/// and also as an HTML attribute in ` Response { + // It uses a rinja 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, + } + + let status = match &self { + AppError::NotFound => StatusCode::NOT_FOUND, + AppError::Render(_) => StatusCode::INTERNAL_SERVER_ERROR, + }; + let template = Tmpl { + lang: Lang::default(), + err: self, + }; + if let Ok(body) = template.render() { + with_status(html(body), status).into_response() + } else { + status.into_response() + } + } +} + +/// This is the first page your user hits, meaning it does not contain language information, +/// so we redirect them. +fn start_handler() -> impl Reply { + redirect::found(Uri::from_static("/en/index.html")) +} + +/// 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). +fn index_handler(lang: Lang, query: IndexHandlerQuery) -> Result { + // 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 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: query.name, + }; + Ok(html(template.render()?)) +} + +#[derive(Debug, Deserialize)] +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 warp will +/// show a "404 - Not Found" message if absent. +fn greeting_handler(lang: Lang, query: GreetingHandlerQuery) -> Result<impl Reply, AppError> { + #[derive(Debug, Template)] + #[template(path = "greet.html")] + struct Tmpl { + lang: Lang, + name: String, + } + + let template = Tmpl { + lang, + name: query.name, + }; + Ok(html(template.render()?)) +} diff --git a/examples/warp-app/templates/_layout.css b/examples/warp-app/templates/_layout.css new file mode 100644 index 00000000..3b40a475 --- /dev/null +++ b/examples/warp-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/warp-app/templates/_layout.html b/examples/warp-app/templates/_layout.html new file mode 100644 index 00000000..a0d8fd5d --- /dev/null +++ b/examples/warp-app/templates/_layout.html @@ -0,0 +1,67 @@ +{#- + This is the basic layout of our example application. + It is the core skeleton 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 it 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. + ~#} + + + + {%~ block content %}{% endblock ~%} + + + +{%- macro lang_select(page, query="") -%} + +{%- endmacro lang_select -%} diff --git a/examples/warp-app/templates/error.html b/examples/warp-app/templates/error.html new file mode 100644 index 00000000..a0f05b12 --- /dev/null +++ b/examples/warp-app/templates/error.html @@ -0,0 +1,25 @@ +{% extends "_layout.html" %} + +{%- block title -%} + {%- match err -%} + {% when AppError::NotFound -%} 404: Not Found + {% else -%} 500: Internal Server Error + {%- endmatch -%} +{%- endblock -%} + +{%- block content -%} +

+ {%- match err -%} + {% when AppError::NotFound -%} 404: Not Found + {% else -%} 500: Internal Server Error + {%- endmatch -%} +

+ + {%- match err -%} + {% when AppError::NotFound -%} + {% when AppError::Render(err) -%} +
{{ err }}
+ {%- endmatch -%} + +

Back to the first page.

+{%- endblock -%} diff --git a/examples/warp-app/templates/greet.html b/examples/warp-app/templates/greet.html new file mode 100644 index 00000000..ff2553b9 --- /dev/null +++ b/examples/warp-app/templates/greet.html @@ -0,0 +1,43 @@ +{% extends "_layout.html" %} + +{%- block title -%} + {%- match lang -%} + {%- when Lang::en -%} Hello, {{name}}! + {%- when Lang::de -%} Hallo, {{name}}! + {%- when Lang::fr -%} Bonjour, {{name}}! + {%- endmatch -%} +{%- endblock -%} + +{%- block content -%} +

+ {%- match lang -%} + {%- when Lang::en -%} Hello! + {%- when Lang::de -%} Hallo! + {%- when Lang::fr -%} Bonjour! + {%- endmatch -%} +

+

+ {%- match lang -%} + {%- when Lang::en -%} + Hello, {{name}}, nice to meet you! {#-~#} + I'm a Rinja example application. + {%- when Lang::de -%} + Hallo, {{name}}, schön dich kennenzulernen! {#-~#} + Ich bin eine Rinja-Beispielanwendung. + {%- when Lang::fr -%} + Bonjour, {{name}}, ravi de vous rencontrer ! {#-~#} + Je suis une application d'exemple de Rinja. + {%- endmatch -%} +

+

+ + {%- match lang -%} + {%- when Lang::en -%} Back to the first page. + {%- when Lang::de -%} Zurück zur ersten Seite. + {%- when Lang::fr -%} Retour à la première page. + {%- endmatch -%} + +

+ + {%- call lang_select("greet-me", name|urlencode|fmt("?name={}")) -%} +{%- endblock -%} diff --git a/examples/warp-app/templates/index.html b/examples/warp-app/templates/index.html new file mode 100644 index 00000000..935dc989 --- /dev/null +++ b/examples/warp-app/templates/index.html @@ -0,0 +1,84 @@ +{% 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! + {%- when Lang::fr -%} Bonjour! + {%- endmatch -%} +{%- endblock -%} + +{%- block content -%} +

+ {%- match lang -%} + {%- when Lang::en -%} Hello! + {%- when Lang::de -%} Hallo! + {%- when Lang::fr -%} Bonjour! + {%- endmatch -%} +

+
+

+ {#- + 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. {#-~#} + Would you please tell me your name? + {%- when Lang::de -%} + Ich möchte dir gerne hallo sagen. {#-~#} + Bitte nenne mir doch deinen Namen! + {%- when Lang::fr -%} + Je voudrais vous dire bonjour. {#-~#} + Pourriez-vous me donner votre nom ? + {%- endmatch -%} +

+

+ +

+

+ +

+
+ + {#- + The called macro is defined in base template "_layout.html", + and used to display the language selection footer. + ~#} + {%- call lang_select("index") -%} +{%- endblock -%} diff --git a/examples/warp-app/tomlfmt.toml b/examples/warp-app/tomlfmt.toml new file mode 120000 index 00000000..f5ea3619 --- /dev/null +++ b/examples/warp-app/tomlfmt.toml @@ -0,0 +1 @@ +../../tomlfmt.toml \ No newline at end of file